Repository: hyperledger/firefly
Pull request #899 (2022-07-12 19:32:00)

Add E2E tests for contract termination
Part of [FIR-12](https://github.com/hyperledger/firefly-fir/pull/12)

~~Depends on https://github.com/hyperledger/firefly-cli/pull/197~~
~~In a chain with #904~~

Add a new E2E test which dynamically:
* deploys two instances of the FireFly multiparty contract
* dynamically adds a new namespace to the config of both nodes
* resets both nodes to read the config
* sends one message on the first contract
* performs a network migration
* sends one message on the new contract

There are variations of this test for both V1->V2 contract migrations and V2->V2. Notably, the V1->V2 migration can only be performed once per stack, so it is not part of the main suites. **We may need to add a dedicated nightly job for this test.**

This PR also includes loads of fixes for things exposed by the E2E tests, including major changes to how the legacy ff_system namespace is managed. There can now only be one multiparty V1 contract _EVER_ used in the history of a FireFly node, and attempting to configure any namespace with a different V1 contract is an error. The ff_system orchestrator will be initialized with that V1 config (if any is found) and will live forever (walking back some of the changes from #884), because we can never be fully sure when all pins from ff_system have been aggregated.

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

> :exclamation: Current head 5b71a52 differs from pull request most recent head 7a81821. Consider uploading reports for the commit 7a81821 to get more accurate results

```diff
@@            Coverage Diff             @@
##             main     #899      +/-   ##
==========================================
+ Coverage   99.95%   99.96%   +0.01%     
==========================================
  Files         298      298              
  Lines       19341    19412      +71     
==========================================
+ Hits        19333    19406      +73     
+ Misses          6        5       -1     
+ Partials        2        1       -1     
```


| [Impacted Files](https://codecov.io/gh/hyperledger/firefly/pull/899?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) | Coverage Δ | |
|---|---|---|
| [internal/blockchain/ethereum/ethereum.go](https://codecov.io/gh/hyperledger/firefly/pull/899/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYmxvY2tjaGFpbi9ldGhlcmV1bS9ldGhlcmV1bS5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/blockchain/ethereum/eventstream.go](https://codecov.io/gh/hyperledger/firefly/pull/899/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYmxvY2tjaGFpbi9ldGhlcmV1bS9ldmVudHN0cmVhbS5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/blockchain/fabric/eventstream.go](https://codecov.io/gh/hyperledger/firefly/pull/899/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYmxvY2tjaGFpbi9mYWJyaWMvZXZlbnRzdHJlYW0uZ28=) | `100.00% <100.00%> (ø)` | |
| [internal/blockchain/fabric/fabric.go](https://codecov.io/gh/hyperledger/firefly/pull/899/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYmxvY2tjaGFpbi9mYWJyaWMvZmFicmljLmdv) | `100.00% <100.00%> (ø)` | |
| [internal/multiparty/manager.go](https://codecov.io/gh/hyperledger/firefly/pull/899/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvbXVsdGlwYXJ0eS9tYW5hZ2VyLmdv) | `100.00% <100.00%> (ø)` | |
| [internal/multiparty/operations.go](https://codecov.io/gh/hyperledger/firefly/pull/899/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvbXVsdGlwYXJ0eS9vcGVyYXRpb25zLmdv) | `100.00% <100.00%> (ø)` | |
| [internal/namespace/manager.go](https://codecov.io/gh/hyperledger/firefly/pull/899/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/apiserver/server.go](https://codecov.io/gh/hyperledger/firefly/pull/899/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.93%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/hyperledger/firefly/pull/899?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/899?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger). Last update [2adbebb...7a81821](https://codecov.io/gh/hyperledger/firefly/pull/899?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).

awrichar: Factored some of the general enhancements out to https://github.com/hyperledger/firefly/pull/904 as a pre-req before adding the new test here.
awrichar: Further, honing of this test has revealed that the migration feature is still a bit broken. Moving back to draft state while addressing the remaining issues.
awrichar: Fixed up the test so that it passes now, so I'm opening this back up for review. I think there may still be work to do to reliably terminate the ff_system namespace though.

Patch:
diff --git a/.gitignore b/.gitignore
index df3c2f769..b275434dd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,8 +6,8 @@ smart_contracts/ethereum/solidity_firefly/build
 smart_contracts/ethereum/solidity_firefly/package-lock.json
 utils/kat-client/node_modules
 **/*.jar
-firefly
-firefly-nocgo
+/firefly
+/firefly-nocgo
 coverage.txt
 **/debug.test
 .DS_Store
@@ -15,4 +15,4 @@ frontend
 __debug*
 !deploy/charts/firefly
 containerlogs
-.vscode/*.log
\ No newline at end of file
+.vscode/*.log
diff --git a/cmd/firefly.go b/cmd/firefly.go
index d7b9bf897..054595c84 100644
--- a/cmd/firefly.go
+++ b/cmd/firefly.go
@@ -118,27 +118,27 @@ func run() error {
 	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
 
 	for {
+		log.L(ctx).Infof("Starting up")
+		rootCtx, rootCancelCtx := context.WithCancel(ctx)
 		mgr := getRootManager()
 		as := apiserver.NewAPIServer()
-		go startFirefly(ctx, mgr, as, errChan)
+		go startFirefly(rootCtx, rootCancelCtx, mgr, as, errChan)
 		select {
 		case sig := <-sigs:
 			log.L(ctx).Infof("Shutting down due to %s", sig.String())
 			cancelCtx()
 			mgr.WaitStop()
 			return nil
-		/*
-			// TODO: seems useful, but there's currently no way to trigger this
-			case <-ctx.Done():
-				log.L(ctx).Infof("Restarting due to configuration change")
-				mgr.WaitStop()
-				// Re-read the configuration
-				coreconfig.Reset()
-				if err := config.ReadConfig(configSuffix, cfgFile); err != nil {
-					cancelCtx()
-					return err
-				}
-		*/
+		case <-rootCtx.Done():
+			log.L(ctx).Infof("Restarting due to configuration change")
+			mgr.WaitStop()
+			// Re-read the configuration
+			coreconfig.Reset()
+			apiserver.InitConfig()
+			if err := config.ReadConfig(configSuffix, cfgFile); err != nil {
+				cancelCtx()
+				return err
+			}
 		case err := <-errChan:
 			cancelCtx()
 			return err
@@ -146,7 +146,7 @@ func run() error {
 	}
 }
 
-func startFirefly(ctx context.Context, mgr namespace.Manager, as apiserver.Server, errChan chan error) {
+func startFirefly(ctx context.Context, cancelCtx context.CancelFunc, mgr namespace.Manager, as apiserver.Server, errChan chan error) {
 	var err error
 	// Start debug listener
 	debugPort := config.GetInt(coreconfig.DebugPort)
@@ -163,7 +163,7 @@ func startFirefly(ctx context.Context, mgr namespace.Manager, as apiserver.Serve
 		log.L(ctx).Debugf("Debug HTTP endpoint listening on localhost:%d", debugPort)
 	}
 
-	if err = mgr.Init(ctx); err != nil {
+	if err = mgr.Init(ctx, cancelCtx); err != nil {
 		errChan <- err
 		return
 	}
diff --git a/db/migrations/postgres/000095_change_namespace_fields.down.sql b/db/migrations/postgres/000095_change_namespace_fields.down.sql
index 9971d262f..23c01d3aa 100644
--- a/db/migrations/postgres/000095_change_namespace_fields.down.sql
+++ b/db/migrations/postgres/000095_change_namespace_fields.down.sql
@@ -6,4 +6,9 @@ ALTER TABLE namespaces ADD COLUMN id UUID;
 ALTER TABLE namespaces ADD COLUMN message_id UUID;
 ALTER TABLE namespaces ADD COLUMN ntype VARCHAR(64);
 ALTER TABLE namespaces DROP COLUMN remote_name;
+
+DROP INDEX transactions_id;
+CREATE UNIQUE INDEX transactions_id ON transactions(id);
+DROP INDEX operations_id;
+CREATE UNIQUE INDEX operations_id ON operations(id);
 COMMIT;
diff --git a/db/migrations/postgres/000095_change_namespace_fields.up.sql b/db/migrations/postgres/000095_change_namespace_fields.up.sql
index 97f3094c4..16c2e1245 100644
--- a/db/migrations/postgres/000095_change_namespace_fields.up.sql
+++ b/db/migrations/postgres/000095_change_namespace_fields.up.sql
@@ -14,4 +14,7 @@ ALTER TABLE namespaces DROP COLUMN ntype;
 ALTER TABLE namespaces ADD COLUMN remote_name VARCHAR(64);
 UPDATE namespaces SET remote_name = name;
 ALTER TABLE namespaces ALTER COLUMN remote_name SET NOT NULL;
+
+DROP INDEX transactions_id;
+CREATE UNIQUE INDEX transactions_id ON transactions(namespace, id);
 COMMIT;
diff --git a/db/migrations/sqlite/000095_change_namespace_fields.down.sql b/db/migrations/sqlite/000095_change_namespace_fields.down.sql
index bf7129886..5df6b4796 100644
--- a/db/migrations/sqlite/000095_change_namespace_fields.down.sql
+++ b/db/migrations/sqlite/000095_change_namespace_fields.down.sql
@@ -5,3 +5,8 @@ ALTER TABLE namespaces ADD COLUMN id UUID;
 ALTER TABLE namespaces ADD COLUMN message_id UUID;
 ALTER TABLE namespaces ADD COLUMN ntype VARCHAR(64);
 ALTER TABLE namespaces DROP COLUMN remote_name;
+
+DROP INDEX transactions_id;
+CREATE UNIQUE INDEX transactions_id ON transactions(id);
+DROP INDEX operations_id;
+CREATE UNIQUE INDEX operations_id ON operations(id);
diff --git a/db/migrations/sqlite/000095_change_namespace_fields.up.sql b/db/migrations/sqlite/000095_change_namespace_fields.up.sql
index f38ca4c96..a7e99f767 100644
--- a/db/migrations/sqlite/000095_change_namespace_fields.up.sql
+++ b/db/migrations/sqlite/000095_change_namespace_fields.up.sql
@@ -10,3 +10,6 @@ ALTER TABLE namespaces DROP COLUMN message_id;
 ALTER TABLE namespaces DROP COLUMN ntype;
 ALTER TABLE namespaces ADD COLUMN remote_name VARCHAR(64);
 UPDATE namespaces SET remote_name = name;
+
+DROP INDEX transactions_id;
+CREATE UNIQUE INDEX transactions_id ON transactions(namespace, id);
diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml
index a368c3017..da5f799f6 100644
--- a/docs/swagger/swagger.yaml
+++ b/docs/swagger/swagger.yaml
@@ -20467,6 +20467,9 @@ paths:
                                     description: The backend identifier of the subscription
                                       for the FireFly BatchPin contract
                                     type: string
+                                  version:
+                                    description: The version of this multiparty contract
+                                    type: integer
                                 type: object
                               location:
                                 description: A blockchain specific contract identifier.
@@ -20500,6 +20503,10 @@ paths:
                                       description: The backend identifier of the subscription
                                         for the FireFly BatchPin contract
                                       type: string
+                                    version:
+                                      description: The version of this multiparty
+                                        contract
+                                      type: integer
                                   type: object
                                 location:
                                   description: A blockchain specific contract identifier.
@@ -28021,6 +28028,9 @@ paths:
                                     description: The backend identifier of the subscription
                                       for the FireFly BatchPin contract
                                     type: string
+                                  version:
+                                    description: The version of this multiparty contract
+                                    type: integer
                                 type: object
                               location:
                                 description: A blockchain specific contract identifier.
@@ -28054,6 +28064,10 @@ paths:
                                       description: The backend identifier of the subscription
                                         for the FireFly BatchPin contract
                                       type: string
+                                    version:
+                                      description: The version of this multiparty
+                                        contract
+                                      type: integer
                                   type: object
                                 location:
                                   description: A blockchain specific contract identifier.
diff --git a/docs/tutorials/custom_contracts/ethereum.md b/docs/tutorials/custom_contracts/ethereum.md
index 28a609b47..2b46b503c 100644
--- a/docs/tutorials/custom_contracts/ethereum.md
+++ b/docs/tutorials/custom_contracts/ethereum.md
@@ -7,6 +7,7 @@ nav_order: 1
 ---
 
 # Work with Ethereum smart contracts
+
 {: .no_toc }
 
 This guide describes the steps to deploy a smart contract to an Ethereum blockchain and use FireFly to interact with it in order to submit transactions, query for states and listening for events.
diff --git a/docs/tutorials/custom_contracts/fabric.md b/docs/tutorials/custom_contracts/fabric.md
index 3dfc8b905..446db968f 100644
--- a/docs/tutorials/custom_contracts/fabric.md
+++ b/docs/tutorials/custom_contracts/fabric.md
@@ -7,6 +7,7 @@ nav_order: 2
 ---
 
 # Work with Hyperledger Fabric chaincodes
+
 {: .no_toc }
 
 This guide describes the steps to deploy a chaincode to a Hyperledger Fabric blockchain and use FireFly to interact with it in order to submit transactions, query for states and listening for events.
@@ -367,13 +368,6 @@ We will use the FFI JSON constructed above and `POST` that to the `/contracts/in
 }
 ```
 
-> **NOTE**: We can broadcast this contract interface conveniently with the help of FireFly Sandbox running at `http://127.0.0.1:5108`
-* Go to the `Contracts Section`
-* Click on `Define a Contract Interface`
-* Select `FFI - FireFly Interface` in the `Interface Fromat` dropdown
-* Copy the `FFI JSON` crafted by you into the `Schema` Field
-* Click on `Run`
-
 ## Create an HTTP API for the contract
 
 Now comes the fun part where we see some of the powerful, developer-friendly features of FireFly. The next thing we're going to do is tell FireFly to build an HTTP API for this chaincode, complete with an OpenAPI Specification and Swagger UI. As part of this, we'll also tell FireFly where the chaincode is on the blockchain.
@@ -422,14 +416,6 @@ We need to copy the `id` field we got in the response from the previous step to
   }
 }
 ```
-> **NOTE**: We can create this Http API conveniently with the help of FireFly Sandbox running at `http://127.0.0.1:5108`
-* Go to the `Contracts Section`
-* Click on `Register a Contract API`
-* Select the name of your broadcasted FFI in the `Contract Interface` dropdown
-* In the `Name` Field, give a name that will be part of the URL for your Http API
-* In the `Chaincode` Field, give your chaincode name for which you wrote the FFI
-* In the `Channel` Field, give the channel name where your chaincode is deployed
-* Click on `Run`
 
 ## View OpenAPI spec for the contract
 
diff --git a/docs/tutorials/fabric_remote.md b/docs/tutorials/fabric_remote.md
deleted file mode 100644
index 53126ca66..000000000
--- a/docs/tutorials/fabric_remote.md
+++ /dev/null
@@ -1,255 +0,0 @@
----
-layout: default
-title: Work with remote fabric network
-parent: pages.tutorials
-nav_order:
----
-
-# Work with remote Hyperledger Fabric Network
-{: .no_toc }
-
-The FireFly CLI makes it quick and easy to create an entire FireFly development environment on your local machine, including a new blockchain from scratch. However, sometimes a developer may want to connect their local FireFly development environment to a Fabric network that already exists on their machine or elsewhere. This guide describes the steps to connect FireFly to an external Hyperledger Fabric blockchain network (not created by the FireFly CLI), including interacting with a custom chaincode in order to submit transactions and query for state.
-
-> **NOTE:** This guide assumes that you are already running a Hyperledger Fabric blockchain network and have a chaicode deployed in that network that you want to connect to with FireFly.
-
-## Table of contents
-{: .no_toc .text-delta }
-
-1. TOC
-{:toc}
-
----
-
-## Creating a FireFly stack with Fabric provider
-
-In the ff init command, pass fabric in the `-b`  flag to select fabric as the blockchain provider. You can also pass an additional `--prompt-names` flag to give actual names for your organizations in the firefly setup.
-
-```
-ff init -b fabric --prompt-names
-```
-
-Choose a stack name. For this guide, I will choose the name `my-remote-fabric`, but you can pick whatever you want.
-
-```
-stack name: my-remote-fabric
-```
-
-For this guide, we will be creating only one supernode for one of the organizations in our fabric netwok and using that organizational identity to interact with the chaincode. You can create multiple supernodes for different organizations in your fabric network by passing here the number of members in your stack.
-
-```
-number of members: 1
-```
-
-Next, you will be prompted to enter a name for your organization and node. If you selected more than 1 member for your stack in the previous step, it will prompt you to give name for each member. For this guide, I will choose the name `my-org` and `my-org-node`, but you can pick whatever you want.
-
-```
-name for org 0: my-org
-name for node 0: my-org-node
-```
-
-At the end of it, you will be greeted with something like :
-
-```
-Stack 'my-remote-fabric' created!
-To start your new stack run:
-
-ff start my-remote-fabric
-```
-
-Next, you need to edit your stack configuration before you run it.
-
-## Configuring the stack to connect to your remote Fabric network
-
-Now that you have created a stack, let's open it up in your favourite code editor to configure it further. By default, the path where it gets created in your system is `$HOME/.firefly/stacks/my-remote-fabric.`
-
-### Add msp folder representing your fabric identity
-
-In `$HOME/.firefly/stacks/my-remote-fabric`, you need to add an msp folder. This msp folder essentially describes the fabric organizational identiy which will be interacting with your chaincode. Inside the msp folder, you must have all the certifactes and the private key to be used by firefly to query/invoke your chaincode.
-
-You should be having this msp folder for an organizational identity from when you would have set up your fabric network. For more details, please refer to [Hyperledger Fabric Documentation for MSP](https://hyperledger-fabric.readthedocs.io/en/latest/msp.html).
-
-Now, you need to mount this msp directory as a docker volume in your fabconnect serivce.
-
-> **NOTE:** All docker-compose.yml related changes should be done in docker-compose.override.yml.
-
-For this, simply copy the fabconnect_0 service as it is from your docker-compose.yml and paste it in your docker-compose.override.yml. Next, in your docker-compose.override.yml, in the volumes section of the fabconnect_0 service, you need to add a new docker volume mapping your host's msp directory to your fabconnect_0 docker container's /etc/firefly/msp directory. 
-
-It will look something like :
-
-```
--/Users/<username>/.firefly/stacks/my-remote-fabric/msp:/etc/firefly/msp
-```
-
-Your docker-compose.override.yml file after editing should look something like :
-
-```
-# Add custom config overrides here
-# See https://docs.docker.com/compose/extends
-version: "2.1"
-services:
-    fabconnect_0:
-        container_name: my-remote-fabric_fabconnect_0
-        image: ghcr.io/hyperledger/firefly-fabconnect@sha256:0dff97610990414d8ceb24859ced7712fa18b71b1d22b6a6f46fd51266413a22
-        command: -f /fabconnect/fabconnect.yaml
-        volumes:
-            - fabconnect_receipts_0:/fabconnect/receipts
-            - fabconnect_events_0:/fabconnect/events
-            - /Users/<username>/.firefly/stacks/my-remote-fabric/runtime/blockchain/fabconnect.yaml:/fabconnect/fabconnect.yaml
-            - /Users/<username>/.firefly/stacks/my-remote-fabric/runtime/blockchain/ccp.yaml:/fabconnect/ccp.yaml
-            - /Users/<username>/.firefly/stacks/my-remote-fabric/msp:/etc/firefly/msp
-            - firefly_fabric:/etc/firefly
-        ports:
-            - 5102:3000
-        depends_on:
-            fabric_ca:
-                condition: service_started
-            fabric_orderer:
-                condition: service_started
-            fabric_peer:
-                condition: service_started
-        healthcheck:
-            test:
-                - CMD
-                - wget
-                - -O
-                - '-'
-                - http://localhost:3000/status
-        logging:
-            driver: json-file
-            options:
-                max-file: "1"
-                max-size: 10m
-```
-
-> **NOTE:** If you had multiple members in your stack, with each member representing a different fabric organizational entity, then you would need to add separate msp folders for all of them in a similar manner
-
-### Edit your fabric connection profile (ccp.yml)
-
-In `$HOME/.firefly/stacks/my-remote-fabric/init/blockchain`, you will find a ccp.yml file which is mounted as a volume in our fabconnect service. This file is also commonly referred to as fabric connection profile.
-
-This ccp.yml file is written to connect to the default localhost containerized fabric network that is created by firefly CLI when you run your stack. You probably don't want to connect to that network since you are viewing this guide. So what you need to do is :
-
-* You need to edit this ccp.yml file and give your fabric network and client details here instead.
-* For the `credentialStore/cryptoStore` path or the `cryptoconfig` path in your ccp.yml, you need to provide the path of your msp folder in your fabconnect docker container : `/etc/firefly/msp`
-* For the `certificates/key` paths in your ccp.yml, you need to provide the paths of corresponding key/certificates in your msp folder in your fabconnect docker container :`/etc/firefly/msp/<path-to-cert-or-key>`
-
-Your ccp.yml file after editing should look something like :
-
-```
-certificateAuthorities:
-    my-fabric-network-ca:
-        tlsCACerts:
-            path: /etc/firefly/msp/tlscacerts/tls-ca-cert.pem
-        url: https://my-fabric-network-ca.my-domain.com:443
-        registrar:
-            enrollId: my-enroll-id
-            enrollSecret: my-enroll-secret
-channels:
-    my-channel:
-        orderers:
-            - my-orderer-node
-        peers:
-            my-peer-node:
-                chaincodeQuery: true
-                endorsingPeer: true
-                eventSource: true
-                ledgerQuery: true
-client:
-    BCCSP:
-        security:
-            default:
-                provider: SW
-            enabled: true
-            hashAlgorithm: SHA2
-            level: 256
-            softVerify: true
-    credentialStore:
-        cryptoStore:
-            path: /etc/firefly/msp
-        path: /etc/firefly/msp
-    cryptoconfig:
-        path: /etc/firefly/msp
-    logging:
-        level: info
-    organization: my-org
-    tlsCerts:
-        client:
-            cert:
-                path: /etc/firefly/msp/signcerts/cert.pem
-            key:
-                path: /etc/firefly/msp/keystore/key.pem
-orderers:
-    my-orderer-node:
-        tlsCACerts:
-            path: /etc/firefly/msp/tlscacerts/tls-ca-cert.pem
-        url: grpcs://my-orderer-node.my-domain.com:7050
-organizations:
-    my-org:
-        certificateAuthorities:
-            - my-fabric-network-ca
-        cryptoPath: /etc/firefly/msp
-        mspid: my-org-mspid
-        peers:
-            - my-peer-node
-peers:
-    my-peer-node:
-        tlsCACerts:
-            path: /etc/firefly/msp/tlscacerts/tls-ca-cert.pem
-        url: grpcs://my-peer-node.my-domain.com:7051
-version: 1.1.0%
-
-```
-
-> **NOTE:** If you had multiple members in your stack, with each member representing a different fabric organizational entity, then you would need to add separate ccp.yml files for all of them in a similar manner.
-
-## Setting up Batchpin chaincode and running your stack
-
-### Deploying batchpin chaincode to fabric network
-
-For Firefly to succeesfully integrate and interact with your remote fabric network, you need to also deploy a Firefly batchpin chaincode in your network setup. This chaincode can be found in the [FireFly git repo](https://github.com/hyperledger/firefly/tree/main/smart_contracts/fabric/firefly-go).
-
-Once you have successfully packaged, installed, approved and commited this chaincode in your remote fabric network, carefully note down the chaincode name you gave and the channel name where this chaincode is deployed for future reference.
-
-> **NOTE:** You can deploy the batchpin chaincode to the same channel where your application chaincode lives or to a separate channel as well.
-
-### Update batchpin chaincode details in firefly core config
-
-In `$HOME/.firefly/stacks/my-remote-fabric/init/config`, you will find a firefly_core_0.yml file which is used by firefly core service in our firefly network setup.
-
-In this file, under the fabconnect section of blockchain-fabric section, you need to change the chaincode value to your batchpin chaincode name, and the channel value to the channel name where your batchpin chaincode was deployed in the previous step.
-
-### Starting your FireFly stack
-
-To start your stack now, simply run:
-
-```
-ff start my-remote-fabric
-```
-
-This may take a minute or two and in the background the FireFly CLI will do the following for you:
-
-- Download Docker images for all of the components of the Supernode
-- Set up configuration between all the components
-- Deploy FireFly's `BatchPin` smart contract to our remote fabric network
-- Register an identity for each member and node in our remote fabric network
-- It will probably also initialize a new blockchain and blockchain node running inside a container and also deploy an `ERC-1155` token smart contract to that local fabric network. This behaviour can be ignored.
-
-After your stack finishes starting it will print out the links to each member's UI and the Sandbox for that node. For this guide, it's just one member.
-
-```
-Web UI for member '0': http://127.0.0.1:5000/ui
-Sandbox UI for member '0': http://127.0.0.1:5108
-
-To see logs for your stack run:
-
-ff logs my-remote-fabric
-
-```
-
-## Integrate your FireFly stack with your Fabric chaincode 
-
-Once your stack is up and running, you can define and broadcast FireFly Interface Document for your remote fabric chaincode. You can refer to [Broadcast the Contract Interface(Fabric)](./custom_contracts/fabric.html#broadcast-the-contract-interface) guide for this.
-
-You can also now create an HTTP API for your fabric chaincode which will help you easily query/invoke all your chaincode methods with your organizational context that you set up in fabconnect. You can refer to [Create an HTTP API for the contract(Fabric)](./custom_contracts/fabric.html#create-an-http-api-for-the-contract) guide for this.
-
-To view the OpenAPI spec for your contract, or to submit transactions, query for states and listen for events, you can further refer to [Work with Hyperledger Fabric chaincodes](./custom_contracts/fabric.html#work-with-hyperledger-fabric-chaincodes) guide.
diff --git a/go.mod b/go.mod
index 7fc7dc313..3b81190f4 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
 	github.com/golang-migrate/migrate/v4 v4.15.2
 	github.com/gorilla/mux v1.8.0
 	github.com/gorilla/websocket v1.5.0
-	github.com/hyperledger/firefly-common v0.1.15
+	github.com/hyperledger/firefly-common v0.1.14
 	github.com/hyperledger/firefly-signer v0.9.12
 	github.com/jarcoal/httpmock v1.1.0
 	github.com/karlseguin/ccache v2.0.3+incompatible
@@ -29,6 +29,7 @@ require (
 	gitlab.com/hfuss/mux-prometheus v0.0.4
 	golang.org/x/net v0.0.0-20220531201128-c960675eff93
 	golang.org/x/text v0.3.7
+	gopkg.in/yaml.v2 v2.4.0
 )
 
 require (
@@ -81,6 +82,5 @@ require (
 	google.golang.org/protobuf v1.28.0 // indirect
 	gopkg.in/ini.v1 v1.66.6 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
-	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 1cacd7213..7f876936a 100644
--- a/go.sum
+++ b/go.sum
@@ -727,8 +727,8 @@ github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT
 github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/hyperledger/firefly-common v0.1.13/go.mod h1:2NqPi5Ud9H6rSlZXkLbotxW7z4EAD89p3/8oNOpm9Gs=
-github.com/hyperledger/firefly-common v0.1.15 h1:hOAMSz+YuGP1ulLstVFoWxrCAB+0OrbnfUO3v4etvEw=
-github.com/hyperledger/firefly-common v0.1.15/go.mod h1:MNbaI2spBsdZYOub6Duj9xueE7Qyu9itOmJ4vE8tjYw=
+github.com/hyperledger/firefly-common v0.1.14 h1:BFobO96kGPCC4UUvzdwdH8X5oXo1NbtqPrASrd9N0kY=
+github.com/hyperledger/firefly-common v0.1.14/go.mod h1:MNbaI2spBsdZYOub6Duj9xueE7Qyu9itOmJ4vE8tjYw=
 github.com/hyperledger/firefly-signer v0.9.12 h1:pCPiGHx1+MbTsIQuRkoQmfWxvpcvtGHVavls0NnH0po=
 github.com/hyperledger/firefly-signer v0.9.12/go.mod h1:GPQRUZOFOAjkLmg8GDjZUjEdUD0gcar+CSVhwltIwyw=
 github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
diff --git a/internal/apiserver/route_get_namespace.go b/internal/apiserver/route_get_namespace.go
index 0c9f5f481..9b682c380 100644
--- a/internal/apiserver/route_get_namespace.go
+++ b/internal/apiserver/route_get_namespace.go
@@ -39,9 +39,11 @@ var getNamespace = &ffapi.Route{
 	JSONOutputCodes: []int{http.StatusOK},
 	Extensions: &coreExtensions{
 		CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) {
-			ns := r.PP["ns"]
-			or := cr.mgr.Orchestrator(ns)
-			return or.GetNamespace(cr.ctx), nil
+			or, err := getOrchestrator(cr.ctx, cr.mgr, routeTagNonDefaultNamespace, r)
+			if err == nil {
+				output = or.GetNamespace(cr.ctx)
+			}
+			return output, err
 		},
 	},
 }
diff --git a/internal/apiserver/route_spi_get_namespace_by_name.go b/internal/apiserver/route_spi_get_namespace_by_name.go
index 0ea161dd6..5ebc4b65b 100644
--- a/internal/apiserver/route_spi_get_namespace_by_name.go
+++ b/internal/apiserver/route_spi_get_namespace_by_name.go
@@ -38,9 +38,11 @@ var spiGetNamespaceByName = &ffapi.Route{
 	JSONOutputCodes: []int{http.StatusOK},
 	Extensions: &coreExtensions{
 		CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) {
-			ns := r.PP["ns"]
-			or := cr.mgr.Orchestrator(ns)
-			return or.GetNamespace(cr.ctx), nil
+			or, err := getOrchestrator(cr.ctx, cr.mgr, routeTagNonDefaultNamespace, r)
+			if err == nil {
+				output = or.GetNamespace(cr.ctx)
+			}
+			return output, err
 		},
 	},
 }
diff --git a/internal/apiserver/route_spi_post_reset.go b/internal/apiserver/route_spi_post_reset.go
new file mode 100644
index 000000000..ff237fe09
--- /dev/null
+++ b/internal/apiserver/route_spi_post_reset.go
@@ -0,0 +1,43 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package apiserver
+
+import (
+	"net/http"
+
+	"github.com/hyperledger/firefly-common/pkg/ffapi"
+	"github.com/hyperledger/firefly/internal/coremsgs"
+)
+
+var spiPostReset = &ffapi.Route{
+	Name:            "spiPostReset",
+	Path:            "reset",
+	Method:          http.MethodPost,
+	PathParams:      nil,
+	QueryParams:     nil,
+	Description:     coremsgs.APIEndpointsAdminPostReset,
+	JSONInputValue:  nil,
+	JSONOutputValue: nil,
+	JSONOutputCodes: []int{http.StatusNoContent},
+	Extensions: &coreExtensions{
+		FilterFactory: nil,
+		CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) {
+			cr.mgr.Reset(cr.ctx)
+			return nil, nil
+		},
+	},
+}
diff --git a/internal/apiserver/spi_routes.go b/internal/apiserver/spi_routes.go
index d51b7ce8c..059d791ba 100644
--- a/internal/apiserver/spi_routes.go
+++ b/internal/apiserver/spi_routes.go
@@ -26,4 +26,5 @@ var spiRoutes = []*ffapi.Route{
 	spiGetOpByID,
 	spiGetOps,
 	spiPatchOpByID,
+	spiPostReset,
 }
diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go
index c529a7ee1..32a980e0c 100644
--- a/internal/blockchain/ethereum/ethereum.go
+++ b/internal/blockchain/ethereum/ethereum.go
@@ -353,6 +353,10 @@ func (e *Ethereum) handleBatchPinEvent(ctx context.Context, location *fftypes.JS
 	var namespace string
 	if subInfo.version == 1 {
 		namespace = nsOrAction
+		if subInfo.namespace != "" && subInfo.namespace != namespace {
+			log.L(ctx).Debugf("Ignoring batch for '%s' received on subscription for '%s'", namespace, subInfo.namespace)
+			return nil
+		}
 	} else {
 		namespace = subInfo.namespace
 	}
@@ -408,22 +412,21 @@ func (e *Ethereum) buildEventLocationString(msgJSON fftypes.JSONObject) string {
 }
 
 func (e *Ethereum) handleMessageBatch(ctx context.Context, messages []interface{}) error {
-	l := log.L(ctx)
-
 	for i, msgI := range messages {
 		msgMap, ok := msgI.(map[string]interface{})
 		if !ok {
-			l.Errorf("Message cannot be parsed as JSON: %+v", msgI)
+			log.L(ctx).Errorf("Message cannot be parsed as JSON: %+v", msgI)
 			return nil // Swallow this and move on
 		}
 		msgJSON := fftypes.JSONObject(msgMap)
 
-		l1 := l.WithField("ethmsgidx", i)
-		ctx1 := log.WithLogger(ctx, l1)
+		logger := log.L(ctx).WithField("ethmsgidx", i)
+		eventCtx, done := context.WithCancel(log.WithLogger(ctx, logger))
+
 		signature := msgJSON.GetString("signature")
 		sub := msgJSON.GetString("subId")
-		l1.Infof("Received '%s' message", signature)
-		l1.Tracef("Message: %+v", msgJSON)
+		logger.Infof("Received '%s' message on '%s'", signature, sub)
+		logger.Tracef("Message: %+v", msgJSON)
 
 		// Matches one of the active FireFly BatchPin subscriptions
 		if subInfo, ok := e.subs[sub]; ok {
@@ -431,24 +434,28 @@ func (e *Ethereum) handleMessageBatch(ctx context.Context, messages []interface{
 				Address: msgJSON.GetString("address"),
 			})
 			if err != nil {
+				done()
 				return err
 			}
 
 			switch signature {
 			case broadcastBatchEventSignature:
-				if err := e.handleBatchPinEvent(ctx1, location, &subInfo, msgJSON); err != nil {
+				if err := e.handleBatchPinEvent(eventCtx, location, &subInfo, msgJSON); err != nil {
+					done()
 					return err
 				}
 			default:
-				l.Infof("Ignoring event with unknown signature: %s", signature)
+				log.L(ctx).Infof("Ignoring event with unknown signature: %s", signature)
 			}
 		} else {
 			// Subscription not recognized - assume it's from a custom contract listener
 			// (event manager will reject it if it's not)
-			if err := e.handleContractEvent(ctx1, msgJSON); err != nil {
+			if err := e.handleContractEvent(eventCtx, msgJSON); err != nil {
+				done()
 				return err
 			}
 		}
+		done()
 	}
 
 	return nil
diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go
index d275c6e30..263b87e8f 100644
--- a/internal/blockchain/fabric/fabric.go
+++ b/internal/blockchain/fabric/fabric.go
@@ -360,6 +360,10 @@ func (f *Fabric) handleBatchPinEvent(ctx context.Context, location *fftypes.JSON
 	var namespace string
 	if subInfo.version == 1 {
 		namespace = nsOrAction
+		if subInfo.namespace != "" && subInfo.namespace != namespace {
+			log.L(ctx).Debugf("Ignoring batch for '%s' received on subscription for '%s'", namespace, subInfo.namespace)
+			return nil
+		}
 	} else {
 		namespace = subInfo.namespace
 	}
@@ -453,22 +457,21 @@ func (f *Fabric) RemoveFireflySubscription(ctx context.Context, subID string) {
 }
 
 func (f *Fabric) handleMessageBatch(ctx context.Context, messages []interface{}) error {
-	l := log.L(ctx)
-
 	for i, msgI := range messages {
 		msgMap, ok := msgI.(map[string]interface{})
 		if !ok {
-			l.Errorf("Message cannot be parsed as JSON: %+v", msgI)
+			log.L(ctx).Errorf("Message cannot be parsed as JSON: %+v", msgI)
 			return nil // Swallow this and move on
 		}
 		msgJSON := fftypes.JSONObject(msgMap)
 
-		l1 := l.WithField("fabmsgidx", i)
-		ctx1 := log.WithLogger(ctx, l1)
+		logger := log.L(ctx).WithField("fabmsgidx", i)
+		eventCtx, done := context.WithCancel(log.WithLogger(ctx, logger))
+
 		eventName := msgJSON.GetString("eventName")
 		sub := msgJSON.GetString("subId")
-		l1.Infof("Received '%s' message", eventName)
-		l1.Tracef("Message: %+v", msgJSON)
+		logger.Infof("Received '%s' message on '%s'", eventName, sub)
+		logger.Tracef("Message: %+v", msgJSON)
 
 		// Matches one of the active FireFly BatchPin subscriptions
 		if subInfo, ok := f.subs[sub]; ok {
@@ -477,24 +480,28 @@ func (f *Fabric) handleMessageBatch(ctx context.Context, messages []interface{})
 				Channel:   subInfo.channel,
 			})
 			if err != nil {
+				done()
 				return err
 			}
 
 			switch eventName {
 			case broadcastBatchEventName:
-				if err := f.handleBatchPinEvent(ctx1, location, &subInfo, msgJSON); err != nil {
+				if err := f.handleBatchPinEvent(eventCtx, location, &subInfo, msgJSON); err != nil {
+					done()
 					return err
 				}
 			default:
-				l.Infof("Ignoring event with unknown name: %s", eventName)
+				log.L(ctx).Infof("Ignoring event with unknown name: %s", eventName)
 			}
 		} else {
 			// Subscription not recognized - assume it's from a custom contract listener
 			// (event manager will reject it if it's not)
 			if err := f.handleContractEvent(ctx, msgJSON); err != nil {
+				done()
 				return err
 			}
 		}
+		done()
 	}
 
 	return nil
diff --git a/internal/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go
index c80c218a1..ae702515b 100644
--- a/internal/coremsgs/en_api_translations.go
+++ b/internal/coremsgs/en_api_translations.go
@@ -63,16 +63,11 @@ var (
 	APIParamsAutometa                       = ffm("api.params.autometa", "When set, FireFly will automatically generate JSON metadata with the upload details")
 	APIParamsContractAPIID                  = ffm("api.params.contractAPIID", "The ID of the contract API")
 
-	APIEndpointsAdminDeleteConfigRecord = ffm("api.endpoints.adminDeleteConfigRecord", "Deletes a configuration record from the database")
-	APIEndpointsAdminGetConfigRecord    = ffm("api.endpoints.adminGetConfigRecord", "Gets a configuration record from the database")
-	APIEndpointsAdminGetConfigRecords   = ffm("api.endpoints.adminGetConfigRecords", "Lists configuration records from the database")
-	APIEndpointsAdminGetConfig          = ffm("api.endpoints.adminGetConfig", "Gets the full configuration for this node")
 	APIEndpointsAdminGetNamespaceByName = ffm("api.endpoints.adminGetNamespaceByName", "Gets a namespace by name")
 	APIEndpointsAdminGetNamespaces      = ffm("api.endpoints.adminGetNamespaces", "List namespaces")
 	APIEndpointsAdminGetOpByID          = ffm("api.endpoints.adminGetOpByID", "Gets an operation by ID")
 	APIEndpointsAdminGetOps             = ffm("api.endpoints.adminGetOps", "Lists operations")
-	APIEndpointsAdminPutConfigRecord    = ffm("api.endpoints.adminPutConfigRecord", "Sets a configuration key in the database. This will override matching configuration keys set in the core config file.")
-	APIEndpointsAdminPostResetConfig    = ffm("api.endpoints.adminPostResetConfig", "Restarts FireFly Core HTTP servers and apply all configuration updates. This will apply any new changes that have been made to the core config file, or through the admin API.")
+	APIEndpointsAdminPostReset          = ffm("api.endpoints.adminPostResetConfig", "Restarts FireFly Core HTTP servers and apply all configuration updates")
 	APIEndpointsAdminPatchOpByID        = ffm("api.endpoints.adminPatchOpByID", "Updates an operation by ID")
 	APIEndpointsAdminGetListenerByID    = ffm("api.endpoints.adminGetListenerByID", "Gets a contract listener by ID")
 	APIEndpointsAdminGetListeners       = ffm("api.endpoints.adminGetListeners", "Lists contract listeners")
diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go
index 402ddbd6b..0f5af182e 100644
--- a/internal/coremsgs/en_error_messages.go
+++ b/internal/coremsgs/en_error_messages.go
@@ -260,4 +260,5 @@ var (
 	MsgInvalidBatchPinEvent               = ffe("FF10418", "BatchPin event is not valid - %s (%s): %s")
 	MsgDuplicatePluginRemoteName          = ffe("FF10419", "Invalid %s plugin remote name: %s - remote names must be unique")
 	MsgInvalidConnectorName               = ffe("FF10420", "Could not find name %s for %s connector")
+	MsgCannotInitLegacyNS                 = ffe("FF10421", "could not initialize legacy '%s' namespace - found conflicting V1 multi-party config in %s and %s")
 )
diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go
index c28a6edea..1f9f94add 100644
--- a/internal/coremsgs/en_struct_descriptions.go
+++ b/internal/coremsgs/en_struct_descriptions.go
@@ -386,6 +386,7 @@ var (
 	MultipartyContractsActive      = ffm("MultipartyContracts.active", "The currently active FireFly smart contract")
 	MultipartyContractsTerminated  = ffm("MultipartyContracts.terminated", "Previously-terminated FireFly smart contracts")
 	MultipartyContractIndex        = ffm("MultipartyContract.index", "The index of this contract in the config file")
+	MultipartyContractVersion      = ffm("MultipartyContract.version", "The version of this multiparty contract")
 	MultipartyContractFinalEvent   = ffm("MultipartyContract.finalEvent", "The identifier for the final blockchain event received from this contract before termination")
 	MultipartyContractFirstEvent   = ffm("MultipartyContract.firstEvent", "A blockchain specific string, such as a block number, to start listening from. The special strings 'oldest' and 'newest' are supported by all blockchain connectors")
 	MultipartyContractLocation     = ffm("MultipartyContract.location", "A blockchain specific contract identifier. For example an Ethereum contract address, or a Fabric chaincode name and channel")
diff --git a/internal/definitions/sender_contracts.go b/internal/definitions/sender_contracts.go
index 0471030c3..d72f04b82 100644
--- a/internal/definitions/sender_contracts.go
+++ b/internal/definitions/sender_contracts.go
@@ -33,6 +33,7 @@ func (bm *definitionSender) DefineFFI(ctx context.Context, ffi *fftypes.FFI, wai
 	}
 
 	if bm.multiparty {
+		ffi.Namespace = bm.namespace // TODO: change validation to not check namespace
 		if err := bm.contracts.ResolveFFI(ctx, ffi); err != nil {
 			return err
 		}
@@ -55,6 +56,7 @@ func (bm *definitionSender) DefineContractAPI(ctx context.Context, httpServerURL
 	api.ID = fftypes.NewUUID()
 
 	if bm.multiparty {
+		api.Namespace = bm.namespace // TODO: change validation to not check namespace
 		if err := bm.contracts.ResolveContractAPI(ctx, httpServerURL, api); err != nil {
 			return err
 		}
diff --git a/internal/events/network_action.go b/internal/events/network_action.go
index 4a8e42df2..296391c9a 100644
--- a/internal/events/network_action.go
+++ b/internal/events/network_action.go
@@ -56,7 +56,7 @@ func (em *eventManager) BlockchainNetworkAction(action string, location *fftypes
 		}
 
 		if err == nil {
-			chainEvent := buildBlockchainEvent(core.LegacySystemNamespace, nil, event, &core.BlockchainTransactionRef{
+			chainEvent := buildBlockchainEvent(em.namespace.LocalName, nil, event, &core.BlockchainTransactionRef{
 				BlockchainID: event.BlockchainTXID,
 			})
 			err = em.maybePersistBlockchainEvent(em.ctx, chainEvent, nil)
diff --git a/internal/multiparty/manager.go b/internal/multiparty/manager.go
index c72f2fe33..ee35bcf4b 100644
--- a/internal/multiparty/manager.go
+++ b/internal/multiparty/manager.go
@@ -39,13 +39,13 @@ type Manager interface {
 	RootOrg() RootOrg
 
 	// ConfigureContract initializes the subscription to the FireFly contract
-	// - Checks the namespace contract info against the plugin's configuration, and updates it as needed
-	// - Initializes the contract info for performing BatchPin transactions, and initializes subscriptions for BatchPin events
+	// - Determines the active multiparty contract entry from the config, and updates the namespace with contract info
+	// - Resolves the multiparty contract address and version, and initializes subscriptions for contract events
 	ConfigureContract(ctx context.Context) (err error)
 
 	// TerminateContract marks the given event as the last one to be parsed on the current FireFly contract
-	// - Validates that the event came from the currently active FireFly contract
-	// - Re-initializes the plugin against the next configured FireFly contract
+	// - Validates that the event came from the currently active multiparty contract
+	// - Re-initializes the plugin against the next configured multiparty contract
 	// - Updates the namespace contract info to record the point of termination and the newly active contract
 	TerminateContract(ctx context.Context, location *fftypes.JSONAny, termination *blockchain.Event) (err error)
 
@@ -81,25 +81,20 @@ type Contract struct {
 }
 
 type multipartyManager struct {
-	ctx            context.Context
-	stop           func()
-	namespace      *core.Namespace
-	database       database.Plugin
-	blockchain     blockchain.Plugin
-	operations     operations.Manager
-	metrics        metrics.Manager
-	txHelper       txcommon.Helper
-	config         Config
-	networkVersion int
+	namespace  *core.Namespace
+	database   database.Plugin
+	blockchain blockchain.Plugin
+	operations operations.Manager
+	metrics    metrics.Manager
+	txHelper   txcommon.Helper
+	config     Config
 }
 
-func NewMultipartyManager(ctx context.Context, stop func(), ns *core.Namespace, config Config, di database.Plugin, bi blockchain.Plugin, om operations.Manager, mm metrics.Manager, th txcommon.Helper) (Manager, error) {
+func NewMultipartyManager(ctx context.Context, ns *core.Namespace, config Config, di database.Plugin, bi blockchain.Plugin, om operations.Manager, mm metrics.Manager, th txcommon.Helper) (Manager, error) {
 	if di == nil || bi == nil || mm == nil || om == nil || th == nil {
 		return nil, i18n.NewError(ctx, coremsgs.MsgInitializationNilDepError, "MultipartyManager")
 	}
 	mp := &multipartyManager{
-		ctx:        ctx,
-		stop:       stop,
 		namespace:  ns,
 		config:     config,
 		database:   di,
@@ -124,7 +119,11 @@ func (mm *multipartyManager) RootOrg() RootOrg {
 }
 
 func (mm *multipartyManager) ConfigureContract(ctx context.Context) (err error) {
-	contracts := &mm.namespace.Contracts
+	return mm.configureContractCommon(ctx, false)
+}
+
+func (mm *multipartyManager) configureContractCommon(ctx context.Context, migration bool) (err error) {
+	contracts := mm.namespace.Contracts
 	log.L(ctx).Infof("Resolving FireFly contract at index %d", contracts.Active.Index)
 	location, firstEvent, err := mm.resolveFireFlyContract(ctx, contracts.Active.Index)
 	if err != nil {
@@ -136,24 +135,20 @@ func (mm *multipartyManager) ConfigureContract(ctx context.Context) (err error)
 		return err
 	}
 
-	if !contracts.Active.Location.IsNil() && contracts.Active.Location.String() != location.String() {
-		log.L(ctx).Warnf("FireFly contract location changed from %s to %s", contracts.Active.Location, location)
-	}
-
-	if mm.namespace.LocalName == core.LegacySystemNamespace && version > 1 {
-		// ff_system namespace should stop its orchestrator for network V2+
-		mm.stop()
-		return nil
+	if !migration {
+		if !contracts.Active.Location.IsNil() && contracts.Active.Location.String() != location.String() {
+			log.L(ctx).Warnf("FireFly contract location changed from %s to %s", contracts.Active.Location, location)
+		}
 	}
 
 	subID, err := mm.blockchain.AddFireflySubscription(ctx, mm.namespace.LocalName, location, firstEvent)
 	if err == nil {
-		mm.networkVersion = version
-		contracts.Active = core.MultipartyContract{
+		contracts.Active = &core.MultipartyContract{
 			Location:   location,
 			FirstEvent: firstEvent,
 			Info: core.MultipartyContractInfo{
 				Subscription: subID,
+				Version:      version,
 			},
 		}
 		err = mm.database.UpsertNamespace(ctx, mm.namespace, true)
@@ -182,21 +177,21 @@ func (mm *multipartyManager) resolveFireFlyContract(ctx context.Context, contrac
 }
 
 func (mm *multipartyManager) TerminateContract(ctx context.Context, location *fftypes.JSONAny, termination *blockchain.Event) (err error) {
-	contracts := &mm.namespace.Contracts
+	contracts := mm.namespace.Contracts
 	if contracts.Active.Location.String() != location.String() {
 		log.L(ctx).Warnf("Ignoring termination event from contract at '%s', which does not match active '%s'", location, contracts.Active.Location)
 		return nil
 	}
 	log.L(ctx).Infof("Processing termination of contract #%d at '%s'", contracts.Active.Index, contracts.Active.Location)
+	mm.blockchain.RemoveFireflySubscription(ctx, contracts.Active.Info.Subscription)
 	contracts.Active.Info.FinalEvent = termination.ProtocolID
 	contracts.Terminated = append(contracts.Terminated, contracts.Active)
-	contracts.Active = core.MultipartyContract{Index: contracts.Active.Index + 1}
-	mm.blockchain.RemoveFireflySubscription(ctx, contracts.Active.Info.Subscription)
-	return mm.ConfigureContract(ctx)
+	contracts.Active = &core.MultipartyContract{Index: contracts.Active.Index + 1}
+	return mm.configureContractCommon(ctx, true)
 }
 
 func (mm *multipartyManager) GetNetworkVersion() int {
-	return mm.networkVersion
+	return mm.namespace.Contracts.Active.Info.Version
 }
 
 func (mm *multipartyManager) SubmitNetworkAction(ctx context.Context, signingKey string, action *core.NetworkAction) error {
diff --git a/internal/namespace/manager.go b/internal/namespace/manager.go
index 99a880415..e22a2a28a 100644
--- a/internal/namespace/manager.go
+++ b/internal/namespace/manager.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"strconv"
 	"sync"
 
 	"github.com/hyperledger/firefly-common/pkg/auth"
@@ -70,9 +71,10 @@ var (
 )
 
 type Manager interface {
-	Init(ctx context.Context) error
+	Init(ctx context.Context, cancelCtx context.CancelFunc) error
 	Start() error
 	WaitStop()
+	Reset(ctx context.Context)
 
 	Orchestrator(ns string) orchestrator.Orchestrator
 	SPIEvents() spievents.Manager
@@ -83,15 +85,15 @@ type Manager interface {
 }
 
 type namespace struct {
-	remoteName   string
-	description  string
+	core.Namespace
 	orchestrator orchestrator.Orchestrator
 	config       orchestrator.Config
-	plugins      orchestrator.Plugins
+	plugins      []string
 }
 
 type namespaceManager struct {
 	ctx         context.Context
+	cancelCtx   context.CancelFunc
 	nsMux       sync.Mutex
 	namespaces  map[string]*namespace
 	pluginNames map[string]bool
@@ -152,6 +154,18 @@ type authPlugin struct {
 	plugin auth.Plugin
 }
 
+func stringSlicesEqual(a, b []string) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for i, v := range a {
+		if v != b[i] {
+			return false
+		}
+	}
+	return true
+}
+
 func NewNamespaceManager(withDefaults bool) Manager {
 	nm := &namespaceManager{
 		namespaces:       make(map[string]*namespace),
@@ -178,8 +192,9 @@ func NewNamespaceManager(withDefaults bool) Manager {
 	return nm
 }
 
-func (nm *namespaceManager) Init(ctx context.Context) (err error) {
+func (nm *namespaceManager) Init(ctx context.Context, cancelCtx context.CancelFunc) (err error) {
 	nm.ctx = ctx
+	nm.cancelCtx = cancelCtx
 
 	if err = nm.loadPlugins(ctx); err != nil {
 		return err
@@ -191,77 +206,124 @@ func (nm *namespaceManager) Init(ctx context.Context) (err error) {
 		return err
 	}
 
-	defaultNS := config.GetString(coreconfig.NamespacesDefault)
-	var systemNS *namespace
+	if nm.metricsEnabled {
+		// Ensure metrics are registered
+		metrics.Registry()
+	}
+
+	// In network version 1, the blockchain plugin and multiparty contract were global and singular.
+	// Therefore, if any namespace was EVER pointed at a V1 contract, that contract and that namespace's plugins
+	// become the de facto configuration for ff_system as well. There can only be one V1 contract in the history
+	// of a given FireFly node, because it's impossible to re-create ff_system against a different contract
+	// or different set of plugins.
+	var v1Namespace *namespace
+	var v1Contract *core.MultipartyContract
 
-	// Start an orchestrator per namespace
-	for name, ns := range nm.namespaces {
-		if err := nm.initNamespace(name, ns); err != nil {
+	for _, ns := range nm.namespaces {
+		if err := nm.initNamespace(ns); err != nil {
 			return err
 		}
-
-		// If the default namespace is a multiparty V1 namespace, insert the legacy ff_system namespace
-		if name == defaultNS && ns.config.Multiparty.Enabled && ns.orchestrator.MultiParty().GetNetworkVersion() == 1 {
-			systemNS = &namespace{}
-			*systemNS = *ns
-			systemNS.remoteName = core.LegacySystemNamespace
-			if err := nm.initNamespace(core.LegacySystemNamespace, systemNS); err != nil {
-				return err
+		multiparty := ns.config.Multiparty.Enabled
+		version := "n/a"
+		if multiparty {
+			version = fmt.Sprintf("%d", ns.Namespace.Contracts.Active.Info.Version)
+		}
+		log.L(ctx).Infof("Initialized namespace '%s' multiparty=%s version=%s", ns.LocalName, strconv.FormatBool(multiparty), version)
+		if multiparty {
+			contract := nm.findV1Contract(ns)
+			if contract != nil {
+				if v1Namespace == nil {
+					v1Namespace = ns
+					v1Contract = contract
+				} else if !stringSlicesEqual(v1Namespace.plugins, ns.plugins) ||
+					v1Contract.Location.String() != contract.Location.String() ||
+					v1Contract.FirstEvent != contract.FirstEvent {
+					return i18n.NewError(ctx, coremsgs.MsgCannotInitLegacyNS, core.LegacySystemNamespace, v1Namespace.LocalName, ns.LocalName)
+				}
 			}
 		}
 	}
 
-	if systemNS != nil {
+	if v1Namespace != nil {
+		systemNS := &namespace{
+			Namespace: v1Namespace.Namespace,
+			config:    v1Namespace.config,
+			plugins:   v1Namespace.plugins,
+		}
+		systemNS.LocalName = core.LegacySystemNamespace
+		systemNS.RemoteName = core.LegacySystemNamespace
 		nm.namespaces[core.LegacySystemNamespace] = systemNS
+		err = nm.initNamespace(systemNS)
+		log.L(ctx).Infof("Initialized namespace '%s' as a copy of '%s'", core.LegacySystemNamespace, v1Namespace.LocalName)
+	}
+	return err
+}
+
+func (nm *namespaceManager) findV1Contract(ns *namespace) *core.MultipartyContract {
+	if ns.Contracts.Active.Info.Version == 1 {
+		return ns.Contracts.Active
+	}
+	for _, contract := range ns.Contracts.Terminated {
+		if contract.Info.Version == 1 {
+			return contract
+		}
 	}
 	return nil
 }
 
-func (nm *namespaceManager) initNamespace(name string, ns *namespace) error {
-	stored, err := ns.plugins.Database.Plugin.GetNamespace(nm.ctx, name)
+func (nm *namespaceManager) initNamespace(ns *namespace) (err error) {
+	var plugins *orchestrator.Plugins
+	if ns.config.Multiparty.Enabled {
+		plugins, err = nm.validateMultiPartyConfig(nm.ctx, ns.LocalName, ns.plugins)
+	} else {
+		plugins, err = nm.validateNonMultipartyConfig(nm.ctx, ns.LocalName, ns.plugins)
+	}
+	if err != nil {
+		return err
+	}
+
+	database := plugins.Database.Plugin
+	existing, err := database.GetNamespace(nm.ctx, ns.LocalName)
 	switch {
 	case err != nil:
 		return err
-	case stored != nil:
-		stored.RemoteName = ns.remoteName
-		stored.Description = ns.description
+	case existing != nil:
+		ns.Created = existing.Created
+		ns.Contracts = existing.Contracts
 		// TODO: should we check for discrepancies in the multiparty contract config?
 	default:
-		stored = &core.Namespace{
-			LocalName:   name,
-			RemoteName:  ns.remoteName,
-			Description: ns.description,
-			Created:     fftypes.Now(),
+		ns.Created = fftypes.Now()
+		ns.Contracts = &core.MultipartyContracts{
+			Active: &core.MultipartyContract{},
 		}
 	}
-	if err = ns.plugins.Database.Plugin.UpsertNamespace(nm.ctx, stored, true); err != nil {
+	if err = database.UpsertNamespace(nm.ctx, &ns.Namespace, true); err != nil {
 		return err
 	}
 
 	or := nm.utOrchestrator
 	if or == nil {
-		or = orchestrator.NewOrchestrator(stored, ns.config, ns.plugins, nm.metrics)
+		or = orchestrator.NewOrchestrator(&ns.Namespace, ns.config, plugins, nm.metrics)
 	}
-	if err := or.Init(nm.ctx); err != nil {
+	ns.orchestrator = or
+	orCtx, orCancel := context.WithCancel(nm.ctx)
+	if err := or.Init(orCtx, orCancel); err != nil {
 		return err
 	}
-	ns.orchestrator = or
+	go func() {
+		<-orCtx.Done()
+		nm.nsMux.Lock()
+		defer nm.nsMux.Unlock()
+		log.L(nm.ctx).Infof("Terminated namespace '%s'", ns.LocalName)
+		delete(nm.namespaces, ns.LocalName)
+	}()
 	return nil
 }
 
 func (nm *namespaceManager) Start() error {
-	if nm.metricsEnabled {
-		// Ensure metrics are registered
-		metrics.Registry()
-	}
 	// Orchestrators must be started before plugins so as not to miss events
-	for name, ns := range nm.namespaces {
-		onStop := func() {
-			nm.nsMux.Lock()
-			defer nm.nsMux.Unlock()
-			delete(nm.namespaces, name)
-		}
-		if err := ns.orchestrator.Start(onStop); err != nil {
+	for _, ns := range nm.namespaces {
+		if err := ns.orchestrator.Start(); err != nil {
 			return err
 		}
 	}
@@ -297,6 +359,16 @@ func (nm *namespaceManager) WaitStop() {
 	nm.adminEvents.WaitStop()
 }
 
+func (nm *namespaceManager) Reset(ctx context.Context) {
+	// Queue a restart of the root context to pick up a configuration change.
+	// Caller is responsible for terminating the passed context to trigger the actual reset
+	// (allows caller to cleanly finish processing the current request/event).
+	go func() {
+		<-ctx.Done()
+		nm.cancelCtx()
+	}()
+}
+
 func (nm *namespaceManager) loadPlugins(ctx context.Context) (err error) {
 	nm.pluginNames = make(map[string]bool)
 	if nm.metrics == nil {
@@ -682,28 +754,27 @@ func (nm *namespaceManager) initPlugins(ctx context.Context) (err error) {
 }
 
 func (nm *namespaceManager) loadNamespaces(ctx context.Context) (err error) {
-	defaultNS := config.GetString(coreconfig.NamespacesDefault)
+	defaultName := config.GetString(coreconfig.NamespacesDefault)
 	size := namespacePredefined.ArraySize()
 	foundDefault := false
-	names := make(map[string]bool, size)
 	for i := 0; i < size; i++ {
 		nsConfig := namespacePredefined.ArrayEntry(i)
 		name := nsConfig.GetString(coreconfig.NamespaceName)
 		if name == "" {
+			log.L(ctx).Warnf("Skipping unnamed entry at namespaces.predefined[%d]", i)
 			continue
 		}
-		if _, ok := names[name]; ok {
+		if _, ok := nm.namespaces[name]; ok {
 			log.L(ctx).Warnf("Duplicate predefined namespace (ignored): %s", name)
 			continue
 		}
-		names[name] = true
-		foundDefault = foundDefault || name == defaultNS
+		foundDefault = foundDefault || name == defaultName
 		if nm.namespaces[name], err = nm.loadNamespace(ctx, name, i, nsConfig); err != nil {
 			return err
 		}
 	}
 	if !foundDefault {
-		return i18n.NewError(ctx, coremsgs.MsgDefaultNamespaceNotFound, defaultNS)
+		return i18n.NewError(ctx, coremsgs.MsgDefaultNamespaceNotFound, defaultName)
 	}
 	return err
 }
@@ -778,8 +849,6 @@ func (nm *namespaceManager) loadNamespace(ctx context.Context, name string, inde
 		DefaultKey:       conf.GetString(coreconfig.NamespaceDefaultKey),
 		TokenRemoteNames: nm.tokenRemoteNames,
 	}
-	var p *orchestrator.Plugins
-	var err error
 	if multipartyEnabled.(bool) {
 		contractsConf := multipartyConf.SubArray(coreconfig.NamespaceMultipartyContract)
 		contractConfArraySize := contractsConf.ArraySize()
@@ -805,24 +874,16 @@ func (nm *namespaceManager) loadNamespace(ctx context.Context, name string, inde
 		config.Multiparty.Org.Key = orgKey
 		config.Multiparty.Org.Description = orgDesc
 		config.Multiparty.Contracts = contracts
-		p, err = nm.validateMultiPartyConfig(ctx, name, plugins)
-	} else {
-		p, err = nm.validateNonMultipartyConfig(ctx, name, plugins)
-	}
-	if err != nil {
-		return nil, err
-	}
-
-	p.Events = make(map[string]events.Plugin, len(nm.plugins.events))
-	for name, entry := range nm.plugins.events {
-		p.Events[name] = entry.plugin
 	}
 
 	return &namespace{
-		remoteName:  remoteName,
-		description: conf.GetString(coreconfig.NamespaceDescription),
-		config:      config,
-		plugins:     *p,
+		Namespace: core.Namespace{
+			LocalName:   name,
+			RemoteName:  remoteName,
+			Description: conf.GetString(coreconfig.NamespaceDescription),
+		},
+		config:  config,
+		plugins: plugins,
 	}, nil
 }
 
@@ -901,6 +962,10 @@ func (nm *namespaceManager) validateMultiPartyConfig(ctx context.Context, name s
 		return nil, i18n.NewError(ctx, coremsgs.MsgNamespaceWrongPluginsMultiparty, name)
 	}
 
+	result.Events = make(map[string]events.Plugin, len(nm.plugins.events))
+	for name, entry := range nm.plugins.events {
+		result.Events[name] = entry.plugin
+	}
 	return &result, nil
 }
 
@@ -955,6 +1020,10 @@ func (nm *namespaceManager) validateNonMultipartyConfig(ctx context.Context, nam
 		return nil, i18n.NewError(ctx, coremsgs.MsgNamespaceNoDatabase, name)
 	}
 
+	result.Events = make(map[string]events.Plugin, len(nm.plugins.events))
+	for name, entry := range nm.plugins.events {
+		result.Events[name] = entry.plugin
+	}
 	return &result, nil
 }
 
@@ -976,7 +1045,7 @@ func (nm *namespaceManager) GetNamespaces(ctx context.Context) ([]*core.Namespac
 	defer nm.nsMux.Unlock()
 	results := make([]*core.Namespace, 0, len(nm.namespaces))
 	for _, ns := range nm.namespaces {
-		results = append(results, ns.orchestrator.GetNamespace(ctx))
+		results = append(results, &ns.Namespace)
 	}
 	return results, nil
 }
diff --git a/internal/orchestrator/bound_callbacks.go b/internal/orchestrator/bound_callbacks.go
index aa7f5c48c..387a4500e 100644
--- a/internal/orchestrator/bound_callbacks.go
+++ b/internal/orchestrator/bound_callbacks.go
@@ -20,11 +20,9 @@ import (
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/internal/events"
 	"github.com/hyperledger/firefly/internal/operations"
-	"github.com/hyperledger/firefly/pkg/blockchain"
 	"github.com/hyperledger/firefly/pkg/core"
 	"github.com/hyperledger/firefly/pkg/dataexchange"
 	"github.com/hyperledger/firefly/pkg/sharedstorage"
-	"github.com/hyperledger/firefly/pkg/tokens"
 )
 
 type boundCallbacks struct {
@@ -44,14 +42,6 @@ func (bc *boundCallbacks) OperationUpdate(plugin core.Named, nsOpID string, stat
 	})
 }
 
-func (bc *boundCallbacks) BatchPinComplete(batch *blockchain.BatchPin, signingKey *core.VerifierRef) error {
-	return bc.ei.BatchPinComplete(batch, signingKey)
-}
-
-func (bc *boundCallbacks) BlockchainNetworkAction(action string, location *fftypes.JSONAny, event *blockchain.Event, signingKey *core.VerifierRef) error {
-	return bc.ei.BlockchainNetworkAction(action, location, event, signingKey)
-}
-
 func (bc *boundCallbacks) DXEvent(event dataexchange.DXEvent) {
 	switch event.Type() {
 	case dataexchange.DXEventTypeTransferResult:
@@ -61,22 +51,6 @@ func (bc *boundCallbacks) DXEvent(event dataexchange.DXEvent) {
 	}
 }
 
-func (bc *boundCallbacks) TokenPoolCreated(plugin tokens.Plugin, pool *tokens.TokenPool) error {
-	return bc.ei.TokenPoolCreated(plugin, pool)
-}
-
-func (bc *boundCallbacks) TokensTransferred(plugin tokens.Plugin, transfer *tokens.TokenTransfer) error {
-	return bc.ei.TokensTransferred(plugin, transfer)
-}
-
-func (bc *boundCallbacks) BlockchainEvent(event *blockchain.EventWithSubscription) error {
-	return bc.ei.BlockchainEvent(event)
-}
-
-func (bc *boundCallbacks) TokensApproved(plugin tokens.Plugin, approval *tokens.TokenApproval) error {
-	return bc.ei.TokensApproved(plugin, approval)
-}
-
 func (bc *boundCallbacks) SharedStorageBatchDownloaded(payloadRef string, data []byte) (*fftypes.UUID, error) {
 	return bc.ei.SharedStorageBatchDownloaded(bc.ss, payloadRef, data)
 }
diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go
index 912b8fb84..5d5e5cd52 100644
--- a/internal/orchestrator/orchestrator.go
+++ b/internal/orchestrator/orchestrator.go
@@ -52,8 +52,8 @@ import (
 
 // Orchestrator is the main interface behind the API, implementing the actions
 type Orchestrator interface {
-	Init(ctx context.Context) error
-	Start(onStop func()) error
+	Init(ctx context.Context, cancelCtx context.CancelFunc) error
+	Start() error
 	WaitStop() // The close itself is performed by canceling the context
 
 	MultiParty() multiparty.Manager             // only for multiparty
@@ -177,11 +177,10 @@ type Config struct {
 type orchestrator struct {
 	ctx            context.Context
 	cancelCtx      context.CancelFunc
-	onStop         func()
 	started        bool
 	namespace      *core.Namespace
 	config         Config
-	plugins        Plugins
+	plugins        *Plugins
 	multiparty     multiparty.Manager       // only for multiparty
 	batch          batch.Manager            // only for multiparty
 	broadcast      broadcast.Manager        // only for multiparty
@@ -203,7 +202,7 @@ type orchestrator struct {
 	txHelper       txcommon.Helper
 }
 
-func NewOrchestrator(ns *core.Namespace, config Config, plugins Plugins, metrics metrics.Manager) Orchestrator {
+func NewOrchestrator(ns *core.Namespace, config Config, plugins *Plugins, metrics metrics.Manager) Orchestrator {
 	or := &orchestrator{
 		namespace: ns,
 		config:    config,
@@ -213,15 +212,15 @@ func NewOrchestrator(ns *core.Namespace, config Config, plugins Plugins, metrics
 	return or
 }
 
-func (or *orchestrator) Init(ctx context.Context) (err error) {
+func (or *orchestrator) Init(ctx context.Context, cancelCtx context.CancelFunc) (err error) {
 	namespaceLog := or.namespace.LocalName
 	if or.namespace.RemoteName != or.namespace.LocalName {
 		namespaceLog += "->" + or.namespace.RemoteName
 	}
 	or.ctx, or.cancelCtx = context.WithCancel(log.WithLogField(ctx, "ns", namespaceLog))
-	err = or.initPlugins(or.ctx)
+	err = or.initComponents(or.ctx)
 	if err == nil {
-		err = or.initComponents(or.ctx)
+		err = or.initHandlers(or.ctx)
 	}
 	// Bind together the blockchain interface callbacks, with the events manager
 	or.bc.ei = or.events
@@ -255,14 +254,10 @@ func (or *orchestrator) tokens() map[string]tokens.Plugin {
 	return result
 }
 
-func (or *orchestrator) Start(onStop func()) (err error) {
-	or.onStop = onStop
+func (or *orchestrator) Start() (err error) {
 	or.data.Start()
 	if or.config.Multiparty.Enabled {
-		err = or.multiparty.ConfigureContract(or.ctx)
-		if err == nil {
-			err = or.batch.Start()
-		}
+		err = or.batch.Start()
 		if err == nil {
 			err = or.broadcast.Start()
 		}
@@ -281,13 +276,6 @@ func (or *orchestrator) Start(onStop func()) (err error) {
 	return err
 }
 
-func (or *orchestrator) stop() {
-	or.cancelCtx()
-	if or.onStop != nil {
-		or.onStop()
-	}
-}
-
 func (or *orchestrator) WaitStop() {
 	if !or.started {
 		return
@@ -363,11 +351,11 @@ func (or *orchestrator) MultiParty() multiparty.Manager {
 	return or.multiparty
 }
 
-func (or *orchestrator) initPlugins(ctx context.Context) (err error) {
+func (or *orchestrator) initHandlers(ctx context.Context) (err error) {
 	or.plugins.Database.Plugin.SetHandler(or.namespace.LocalName, or)
 
 	if or.plugins.Blockchain.Plugin != nil {
-		or.plugins.Blockchain.Plugin.SetHandler(or.namespace.RemoteName, &or.bc)
+		or.plugins.Blockchain.Plugin.SetHandler(or.namespace.RemoteName, or.events)
 		or.plugins.Blockchain.Plugin.SetOperationHandler(or.namespace.LocalName, &or.bc)
 	}
 
@@ -392,7 +380,7 @@ func (or *orchestrator) initPlugins(ctx context.Context) (err error) {
 	}
 
 	for _, token := range or.plugins.Tokens {
-		if err := token.Plugin.SetHandler(or.namespace.LocalName, &or.bc); err != nil {
+		if err := token.Plugin.SetHandler(or.namespace.LocalName, or.events); err != nil {
 			return err
 		}
 		token.Plugin.SetOperationHandler(or.namespace.LocalName, &or.bc)
@@ -415,11 +403,14 @@ func (or *orchestrator) initManagers(ctx context.Context) (err error) {
 
 	if or.config.Multiparty.Enabled {
 		if or.multiparty == nil {
-			or.multiparty, err = multiparty.NewMultipartyManager(or.ctx, or.stop, or.namespace, or.config.Multiparty, or.database(), or.blockchain(), or.operations, or.metrics, or.txHelper)
+			or.multiparty, err = multiparty.NewMultipartyManager(or.ctx, or.namespace, or.config.Multiparty, or.database(), or.blockchain(), or.operations, or.metrics, or.txHelper)
 			if err != nil {
 				return err
 			}
 		}
+		if err = or.multiparty.ConfigureContract(ctx); err != nil {
+			return err
+		}
 	}
 
 	if or.identity == nil {
diff --git a/internal/orchestrator/status.go b/internal/orchestrator/status.go
index 033b74535..87d5c0bef 100644
--- a/internal/orchestrator/status.go
+++ b/internal/orchestrator/status.go
@@ -112,7 +112,7 @@ func (or *orchestrator) GetStatus(ctx context.Context) (status *core.NamespaceSt
 	}
 
 	if or.config.Multiparty.Enabled {
-		status.Multiparty.Contracts = &or.namespace.Contracts
+		status.Multiparty.Contracts = or.namespace.Contracts
 	}
 
 	if org != nil {
diff --git a/internal/tokens/fftokens/fftokens.go b/internal/tokens/fftokens/fftokens.go
index 847d705fe..9b1e4035e 100644
--- a/internal/tokens/fftokens/fftokens.go
+++ b/internal/tokens/fftokens/fftokens.go
@@ -526,25 +526,28 @@ func (ft *FFTokens) handleMessage(ctx context.Context, msgBytes []byte) (err err
 	}
 
 	l.Debugf("Received %s event %s", msg.Event, msg.ID)
+	eventCtx, done := context.WithCancel(ctx)
+	defer done()
+
 	switch msg.Event {
 	case messageReceipt:
-		ft.handleReceipt(ctx, msg.Data)
+		ft.handleReceipt(eventCtx, msg.Data)
 	case messageBatch:
 		for _, msg := range msg.Data.GetObjectArray("events") {
-			if err = ft.handleMessage(ctx, []byte(msg.String())); err != nil {
+			if err = ft.handleMessage(eventCtx, []byte(msg.String())); err != nil {
 				break
 			}
 		}
 	case messageTokenPool:
-		err = ft.handleTokenPoolCreate(ctx, msg.Data)
+		err = ft.handleTokenPoolCreate(eventCtx, msg.Data)
 	case messageTokenMint:
-		err = ft.handleTokenTransfer(ctx, core.TokenTransferTypeMint, msg.Data)
+		err = ft.handleTokenTransfer(eventCtx, core.TokenTransferTypeMint, msg.Data)
 	case messageTokenBurn:
-		err = ft.handleTokenTransfer(ctx, core.TokenTransferTypeBurn, msg.Data)
+		err = ft.handleTokenTransfer(eventCtx, core.TokenTransferTypeBurn, msg.Data)
 	case messageTokenTransfer:
-		err = ft.handleTokenTransfer(ctx, core.TokenTransferTypeTransfer, msg.Data)
+		err = ft.handleTokenTransfer(eventCtx, core.TokenTransferTypeTransfer, msg.Data)
 	case messageTokenApproval:
-		err = ft.handleTokenApproval(ctx, msg.Data)
+		err = ft.handleTokenApproval(eventCtx, msg.Data)
 	default:
 		l.Errorf("Message unexpected: %s", msg.Event)
 	}
diff --git a/manifest.json b/manifest.json
index d6fddb98a..69e37d01f 100644
--- a/manifest.json
+++ b/manifest.json
@@ -44,6 +44,6 @@
     "release": "v1.1.0-alpha.1"
   },
   "cli": {
-    "tag": "v1.1.0-alpha.1"
+    "tag": "v1.1.0-alpha.2"
   }
 }
diff --git a/mocks/namespacemocks/manager.go b/mocks/namespacemocks/manager.go
index 3dfb07189..7592e4a5d 100644
--- a/mocks/namespacemocks/manager.go
+++ b/mocks/namespacemocks/manager.go
@@ -80,13 +80,13 @@ func (_m *Manager) GetOperationByNamespacedID(ctx context.Context, nsOpID string
 	return r0, r1
 }
 
-// Init provides a mock function with given fields: ctx
-func (_m *Manager) Init(ctx context.Context) error {
-	ret := _m.Called(ctx)
+// Init provides a mock function with given fields: ctx, cancelCtx
+func (_m *Manager) Init(ctx context.Context, cancelCtx context.CancelFunc) error {
+	ret := _m.Called(ctx, cancelCtx)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
-		r0 = rf(ctx)
+	if rf, ok := ret.Get(0).(func(context.Context, context.CancelFunc) error); ok {
+		r0 = rf(ctx, cancelCtx)
 	} else {
 		r0 = ret.Error(0)
 	}
@@ -110,6 +110,11 @@ func (_m *Manager) Orchestrator(ns string) orchestrator.Orchestrator {
 	return r0
 }
 
+// Reset provides a mock function with given fields: ctx
+func (_m *Manager) Reset(ctx context.Context) {
+	_m.Called(ctx)
+}
+
 // ResolveOperationByNamespacedID provides a mock function with given fields: ctx, nsOpID, op
 func (_m *Manager) ResolveOperationByNamespacedID(ctx context.Context, nsOpID string, op *core.OperationUpdateDTO) error {
 	ret := _m.Called(ctx, nsOpID, op)
diff --git a/mocks/orchestratormocks/orchestrator.go b/mocks/orchestratormocks/orchestrator.go
index 48eee1bad..9cfcf99ae 100644
--- a/mocks/orchestratormocks/orchestrator.go
+++ b/mocks/orchestratormocks/orchestrator.go
@@ -1122,13 +1122,13 @@ func (_m *Orchestrator) GetTransactions(ctx context.Context, filter database.And
 	return r0, r1, r2
 }
 
-// Init provides a mock function with given fields: ctx
-func (_m *Orchestrator) Init(ctx context.Context) error {
-	ret := _m.Called(ctx)
+// Init provides a mock function with given fields: ctx, cancelCtx
+func (_m *Orchestrator) Init(ctx context.Context, cancelCtx context.CancelFunc) error {
+	ret := _m.Called(ctx, cancelCtx)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
-		r0 = rf(ctx)
+	if rf, ok := ret.Get(0).(func(context.Context, context.CancelFunc) error); ok {
+		r0 = rf(ctx, cancelCtx)
 	} else {
 		r0 = ret.Error(0)
 	}
@@ -1223,13 +1223,13 @@ func (_m *Orchestrator) RequestReply(ctx context.Context, msg *core.MessageInOut
 	return r0, r1
 }
 
-// Start provides a mock function with given fields: onStop
-func (_m *Orchestrator) Start(onStop func()) error {
-	ret := _m.Called(onStop)
+// Start provides a mock function with given fields:
+func (_m *Orchestrator) Start() error {
+	ret := _m.Called()
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(func()) error); ok {
-		r0 = rf(onStop)
+	if rf, ok := ret.Get(0).(func() error); ok {
+		r0 = rf()
 	} else {
 		r0 = ret.Error(0)
 	}
diff --git a/pkg/core/namespace.go b/pkg/core/namespace.go
index 45482f65c..c96276434 100644
--- a/pkg/core/namespace.go
+++ b/pkg/core/namespace.go
@@ -28,18 +28,19 @@ import (
 // Namespace is an isolated set of named resources, to allow multiple applications to co-exist in the same network, with the same named objects.
 // Can be used for use case segregation, or multi-tenancy.
 type Namespace struct {
-	LocalName   string              `ffstruct:"Namespace" json:"name"`
-	RemoteName  string              `ffstruct:"Namespace" json:"remoteName"`
-	Description string              `ffstruct:"Namespace" json:"description"`
-	Created     *fftypes.FFTime     `ffstruct:"Namespace" json:"created" ffexcludeinput:"true"`
-	Contracts   MultipartyContracts `ffstruct:"Namespace" json:"-"`
+	LocalName   string               `ffstruct:"Namespace" json:"name"`
+	RemoteName  string               `ffstruct:"Namespace" json:"remoteName"`
+	Description string               `ffstruct:"Namespace" json:"description"`
+	Created     *fftypes.FFTime      `ffstruct:"Namespace" json:"created" ffexcludeinput:"true"`
+	Contracts   *MultipartyContracts `ffstruct:"Namespace" json:"-"`
 }
 
 type MultipartyContracts struct {
-	Active     MultipartyContract   `ffstruct:"MultipartyContracts" json:"active"`
-	Terminated []MultipartyContract `ffstruct:"MultipartyContracts" json:"terminated,omitempty"`
+	Active     *MultipartyContract   `ffstruct:"MultipartyContracts" json:"active"`
+	Terminated []*MultipartyContract `ffstruct:"MultipartyContracts" json:"terminated,omitempty"`
 }
 
+// MultipartyContract represents identifying details about a FireFly multiparty contract, as read from the config file
 type MultipartyContract struct {
 	Index      int                    `ffstruct:"MultipartyContract" json:"index"`
 	Location   *fftypes.JSONAny       `ffstruct:"MultipartyContract" json:"location,omitempty"`
@@ -47,9 +48,11 @@ type MultipartyContract struct {
 	Info       MultipartyContractInfo `ffstruct:"MultipartyContract" json:"info"`
 }
 
+// MultipartyContractInfo stores additional info about the FireFly multiparty contract that is computed during node operation
 type MultipartyContractInfo struct {
 	Subscription string `ffstruct:"MultipartyContract" json:"subscription,omitempty"`
 	FinalEvent   string `ffstruct:"MultipartyContract" json:"finalEvent,omitempty"`
+	Version      int    `ffstruct:"MultipartyContract" json:"version,omitempty"`
 }
 
 type NamespaceRef struct {


Test patch:
diff --git a/cmd/firefly_test.go b/cmd/firefly_test.go
index abaae93c7..167fa389f 100644
--- a/cmd/firefly_test.go
+++ b/cmd/firefly_test.go
@@ -91,6 +91,58 @@ func TestExecOkExitSIGINT(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+func TestExecOkRestartThenExit(t *testing.T) {
+	o := &namespacemocks.Manager{}
+	var orContext context.Context
+	initCount := 0
+	init := o.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	init.RunFn = func(a mock.Arguments) {
+		orContext = a[0].(context.Context)
+		cancelOrContext := a[1].(context.CancelFunc)
+		initCount++
+		if initCount == 2 {
+			init.ReturnArguments = mock.Arguments{fmt.Errorf("second run")}
+		}
+		cancelOrContext()
+	}
+	o.On("Start").Return(nil)
+	ws := o.On("WaitStop")
+	ws.RunFn = func(a mock.Arguments) {
+		<-orContext.Done()
+	}
+	_utManager = o
+	defer func() { _utManager = nil }()
+
+	os.Chdir(configDir)
+	err := Execute()
+	assert.EqualError(t, err, "second run")
+}
+
+func TestExecOkRestartConfigProblem(t *testing.T) {
+	o := &namespacemocks.Manager{}
+	tmpDir, err := os.MkdirTemp(os.TempDir(), "ut")
+	assert.NoError(t, err)
+	defer os.RemoveAll(tmpDir)
+	var orContext context.Context
+	init := o.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	init.RunFn = func(a mock.Arguments) {
+		orContext = a[0].(context.Context)
+		cancelOrContext := a[1].(context.CancelFunc)
+		cancelOrContext()
+	}
+	o.On("Start").Return(nil)
+	o.On("WaitStop").Run(func(args mock.Arguments) {
+		<-orContext.Done()
+		os.Chdir(tmpDir) // this will mean we fail to read the config
+	})
+	_utManager = o
+	defer func() { _utManager = nil }()
+
+	os.Chdir(configDir)
+	err = Execute()
+	assert.Regexp(t, "Config File.*Not Found", err)
+}
+
 func TestAPIServerError(t *testing.T) {
 	o := &namespacemocks.Manager{}
 	o.On("Init", mock.Anything, mock.Anything).Return(nil)
@@ -99,7 +151,7 @@ func TestAPIServerError(t *testing.T) {
 	as.On("Serve", mock.Anything, o).Return(fmt.Errorf("pop"))
 
 	errChan := make(chan error)
-	go startFirefly(context.Background(), o, as, errChan)
+	go startFirefly(context.Background(), func() {}, o, as, errChan)
 	err := <-errChan
 	assert.EqualError(t, err, "pop")
 }
diff --git a/internal/apiserver/route_get_namespace_test.go b/internal/apiserver/route_get_namespace_test.go
index cea653375..ff98c31c7 100644
--- a/internal/apiserver/route_get_namespace_test.go
+++ b/internal/apiserver/route_get_namespace_test.go
@@ -17,6 +17,7 @@
 package apiserver
 
 import (
+	"context"
 	"net/http/httptest"
 	"testing"
 
@@ -38,3 +39,17 @@ func TestGetNamespace(t *testing.T) {
 
 	assert.Equal(t, 200, res.Result().StatusCode)
 }
+
+func TestGetNamespaceInvalid(t *testing.T) {
+	mgr, o, as := newTestServer()
+	r := as.createMuxRouter(context.Background(), mgr)
+	o.On("Authorize", mock.Anything, mock.Anything).Return(nil)
+	req := httptest.NewRequest("GET", "/api/v1/namespaces/BAD", nil)
+	req.Header.Set("Content-Type", "application/json; charset=utf-8")
+	res := httptest.NewRecorder()
+
+	mgr.On("Orchestrator", "BAD").Return(nil, nil)
+	r.ServeHTTP(res, req)
+
+	assert.Equal(t, 404, res.Result().StatusCode)
+}
diff --git a/internal/apiserver/route_spi_post_reset_test.go b/internal/apiserver/route_spi_post_reset_test.go
new file mode 100644
index 000000000..781a54e47
--- /dev/null
+++ b/internal/apiserver/route_spi_post_reset_test.go
@@ -0,0 +1,39 @@
+// Copyright © 2021 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package apiserver
+
+import (
+	"bytes"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func TestAdminPostResetConfig(t *testing.T) {
+	mgr, _, as := newTestServer()
+	r := as.createAdminMuxRouter(mgr)
+	req := httptest.NewRequest("POST", "/spi/v1/reset", bytes.NewReader([]byte(`{}`)))
+	req.Header.Set("Content-Type", "application/json; charset=utf-8")
+	res := httptest.NewRecorder()
+
+	mgr.On("Reset", mock.Anything).Return()
+	r.ServeHTTP(res, req)
+
+	assert.Equal(t, 204, res.Result().StatusCode)
+}
diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go
index 87a580f8a..7f303d55d 100644
--- a/internal/blockchain/ethereum/ethereum_test.go
+++ b/internal/blockchain/ethereum/ethereum_test.go
@@ -1042,6 +1042,50 @@ func TestHandleMessageBatchPinOK(t *testing.T) {
 
 }
 
+func TestHandleMessageBatchPinWrongNS(t *testing.T) {
+	data := fftypes.JSONAnyPtr(`
+[
+  {
+		"address": "0x1C197604587F046FD40684A8f21f4609FB811A7b",
+		"blockNumber": "38011",
+		"transactionIndex": "0x0",
+		"transactionHash": "0xc26df2bf1a733e9249372d61eb11bd8662d26c8129df76890b1beb2f6fa72628",
+		"data": {
+			"author": "0X91D2B4381A4CD5C7C0F27565A7D4B829844C8635",
+			"namespace": "ns2",
+			"uuids": "0xe19af8b390604051812d7597d19adfb9847d3bfd074249efb65d3fed15f5b0a6",
+			"batchHash": "0xd71eb138d74c229a388eb0e1abc03f4c7cbb21d4fc4b839fbf0ec73e4263f6be",
+			"payloadRef": "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD",
+			"contexts": [
+				"0x68e4da79f805bca5b912bcda9c63d03e6e867108dabb9b944109aea541ef522a",
+				"0x19b82093de5ce92a01e333048e877e2374354bf846dd034864ef6ffbd6438771"
+			]
+    },
+		"subId": "sb-b5b97a4e-a317-4053-6400-1474650efcb5",
+		"signature": "BatchPin(address,uint256,string,bytes32,bytes32,string,bytes32[])",
+		"logIndex": "50",
+		"timestamp": "1620576488"
+  }
+]`)
+
+	e := &Ethereum{
+		callbacks: common.NewBlockchainCallbacks(),
+	}
+
+	e.subs = map[string]subscriptionInfo{}
+	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = subscriptionInfo{
+		namespace: "ns1",
+		version:   1,
+	}
+
+	var events []interface{}
+	err := json.Unmarshal(data.Bytes(), &events)
+	assert.NoError(t, err)
+	err = e.handleMessageBatch(context.Background(), events)
+	assert.NoError(t, err)
+
+}
+
 func TestHandleMessageBatchPinV2(t *testing.T) {
 	data := fftypes.JSONAnyPtr(`
 [
@@ -1052,7 +1096,6 @@ func TestHandleMessageBatchPinV2(t *testing.T) {
 		"transactionHash": "0xc26df2bf1a733e9249372d61eb11bd8662d26c8129df76890b1beb2f6fa72628",
 		"data": {
 			"author": "0X91D2B4381A4CD5C7C0F27565A7D4B829844C8635",
-			"namespace": "",
 			"uuids": "0xe19af8b390604051812d7597d19adfb9847d3bfd074249efb65d3fed15f5b0a6",
 			"batchHash": "0xd71eb138d74c229a388eb0e1abc03f4c7cbb21d4fc4b839fbf0ec73e4263f6be",
 			"payloadRef": "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD",
diff --git a/internal/database/sqlcommon/namespace_sql_test.go b/internal/database/sqlcommon/namespace_sql_test.go
index fc8dccda4..e5b21b020 100644
--- a/internal/database/sqlcommon/namespace_sql_test.go
+++ b/internal/database/sqlcommon/namespace_sql_test.go
@@ -41,8 +41,8 @@ func TestNamespacesE2EWithDB(t *testing.T) {
 		LocalName:  "namespace1",
 		RemoteName: "default",
 		Created:    fftypes.Now(),
-		Contracts: core.MultipartyContracts{
-			Active: core.MultipartyContract{
+		Contracts: &core.MultipartyContracts{
+			Active: &core.MultipartyContract{
 				Index: 1,
 			},
 		},
diff --git a/internal/events/network_action_test.go b/internal/events/network_action_test.go
index 026292518..e001c1ee8 100644
--- a/internal/events/network_action_test.go
+++ b/internal/events/network_action_test.go
@@ -48,7 +48,7 @@ func TestNetworkAction(t *testing.T) {
 	mii := em.identity.(*identitymanagermocks.Manager)
 
 	mii.On("FindIdentityForVerifier", em.ctx, []core.IdentityType{core.IdentityTypeOrg}, verifier).Return(&core.Identity{}, nil)
-	mdi.On("GetBlockchainEventByProtocolID", em.ctx, "ff_system", (*fftypes.UUID)(nil), "0001").Return(nil, nil)
+	mdi.On("GetBlockchainEventByProtocolID", em.ctx, "ns1", (*fftypes.UUID)(nil), "0001").Return(nil, nil)
 	mth.On("InsertBlockchainEvent", em.ctx, mock.MatchedBy(func(be *core.BlockchainEvent) bool {
 		return be.ProtocolID == "0001"
 	})).Return(nil)
diff --git a/internal/multiparty/manager_test.go b/internal/multiparty/manager_test.go
index c60e7aa62..8a10f90bd 100644
--- a/internal/multiparty/manager_test.go
+++ b/internal/multiparty/manager_test.go
@@ -57,8 +57,13 @@ func newTestMultipartyManager() *testMultipartyManager {
 		mmi: &metricsmocks.Manager{},
 		mth: &txcommonmocks.Helper{},
 		multipartyManager: multipartyManager{
-			ctx:       context.Background(),
-			namespace: &core.Namespace{LocalName: "ns1", RemoteName: "ns1"},
+			namespace: &core.Namespace{
+				LocalName:  "ns1",
+				RemoteName: "ns1",
+				Contracts: &core.MultipartyContracts{
+					Active: &core.MultipartyContract{},
+				},
+			},
 		},
 	}
 
@@ -82,7 +87,7 @@ func TestNewMultipartyManager(t *testing.T) {
 		core.OpTypeBlockchainNetworkAction,
 	}).Return()
 	ns := &core.Namespace{LocalName: "ns1", RemoteName: "ns1"}
-	nm, err := NewMultipartyManager(context.Background(), func() {}, ns, config, mdi, mbi, mom, mmi, mth)
+	nm, err := NewMultipartyManager(context.Background(), ns, config, mdi, mbi, mom, mmi, mth)
 	assert.NotNil(t, nm)
 	assert.NoError(t, err)
 	assert.Equal(t, "MultipartyManager", nm.Name())
@@ -91,7 +96,7 @@ func TestNewMultipartyManager(t *testing.T) {
 
 func TestInitFail(t *testing.T) {
 	config := Config{Contracts: []Contract{}}
-	_, err := NewMultipartyManager(context.Background(), func() {}, &core.Namespace{}, config, nil, nil, nil, nil, nil)
+	_, err := NewMultipartyManager(context.Background(), &core.Namespace{}, config, nil, nil, nil, nil, nil)
 	assert.Regexp(t, "FF10128", err)
 }
 
@@ -131,8 +136,8 @@ func TestConfigureContractLocationChanged(t *testing.T) {
 	mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
 	mp.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
 
-	mp.multipartyManager.namespace.Contracts = core.MultipartyContracts{
-		Active: core.MultipartyContract{
+	mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
+		Active: &core.MultipartyContract{
 			Index:    0,
 			Location: location,
 		},
@@ -146,32 +151,6 @@ func TestConfigureContractLocationChanged(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-func TestConfigureContractTerminateSystem(t *testing.T) {
-	location := fftypes.JSONAnyPtr(fftypes.JSONObject{
-		"address": "0x123",
-	}.String())
-
-	mp := newTestMultipartyManager()
-	defer mp.cleanup(t)
-	stopped := false
-	mp.stop = func() { stopped = true }
-	mp.namespace.LocalName = core.LegacySystemNamespace
-
-	mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(2, nil)
-
-	mp.multipartyManager.namespace.Contracts = core.MultipartyContracts{
-		Active: core.MultipartyContract{Index: 0},
-	}
-	mp.multipartyManager.config.Contracts = []Contract{{
-		FirstEvent: "0",
-		Location:   location,
-	}}
-
-	err := mp.ConfigureContract(context.Background())
-	assert.NoError(t, err)
-	assert.True(t, stopped)
-}
-
 func TestResolveContractDeprecatedConfig(t *testing.T) {
 	mp := newTestMultipartyManager()
 	defer mp.cleanup(t)
@@ -220,8 +199,8 @@ func TestConfigureContractBadIndex(t *testing.T) {
 	mp := newTestMultipartyManager()
 	defer mp.cleanup(t)
 
-	mp.multipartyManager.namespace.Contracts = core.MultipartyContracts{
-		Active: core.MultipartyContract{Index: 1},
+	mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
+		Active: &core.MultipartyContract{Index: 1},
 	}
 	mp.multipartyManager.config.Contracts = []Contract{{
 		FirstEvent: "0",
@@ -242,8 +221,8 @@ func TestConfigureContractNetworkVersionFail(t *testing.T) {
 
 	mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(0, fmt.Errorf("pop"))
 
-	mp.multipartyManager.namespace.Contracts = core.MultipartyContracts{
-		Active: core.MultipartyContract{Index: 0},
+	mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
+		Active: &core.MultipartyContract{Index: 0},
 	}
 	mp.multipartyManager.config.Contracts = []Contract{{
 		FirstEvent: "0",
@@ -263,8 +242,8 @@ func TestSubmitNetworkAction(t *testing.T) {
 	mp := newTestMultipartyManager()
 	defer mp.cleanup(t)
 
-	mp.multipartyManager.namespace.Contracts = core.MultipartyContracts{
-		Active: core.MultipartyContract{Index: 0},
+	mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
+		Active: &core.MultipartyContract{Index: 0},
 	}
 	mp.multipartyManager.config.Contracts = []Contract{{
 		FirstEvent: "0",
@@ -306,8 +285,8 @@ func TestSubmitNetworkActionTXFail(t *testing.T) {
 	mp := newTestMultipartyManager()
 	defer mp.cleanup(t)
 
-	mp.multipartyManager.namespace.Contracts = core.MultipartyContracts{
-		Active: core.MultipartyContract{Index: 0},
+	mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
+		Active: &core.MultipartyContract{Index: 0},
 	}
 	mp.multipartyManager.config.Contracts = []Contract{{
 		FirstEvent: "0",
@@ -337,8 +316,8 @@ func TestSubmitNetworkActionOpFail(t *testing.T) {
 	mp := newTestMultipartyManager()
 	defer mp.cleanup(t)
 
-	mp.multipartyManager.namespace.Contracts = core.MultipartyContracts{
-		Active: core.MultipartyContract{Index: 0},
+	mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
+		Active: &core.MultipartyContract{Index: 0},
 	}
 	mp.multipartyManager.config.Contracts = []Contract{{
 		FirstEvent: "0",
@@ -374,8 +353,8 @@ func TestSubmitNetworkActionBadType(t *testing.T) {
 	mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
 	mp.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
 
-	mp.multipartyManager.namespace.Contracts = core.MultipartyContracts{
-		Active: core.MultipartyContract{Index: 0},
+	mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
+		Active: &core.MultipartyContract{Index: 0},
 	}
 	mp.multipartyManager.config.Contracts = []Contract{{
 		FirstEvent: "0",
@@ -502,8 +481,8 @@ func TestGetNetworkVersion(t *testing.T) {
 	mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
 	mp.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
 
-	mp.multipartyManager.namespace.Contracts = core.MultipartyContracts{
-		Active: core.MultipartyContract{Index: 0},
+	mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
+		Active: &core.MultipartyContract{Index: 0},
 	}
 	mp.multipartyManager.config.Contracts = []Contract{{
 		FirstEvent: "0",
@@ -530,8 +509,8 @@ func TestConfgureAndTerminateContract(t *testing.T) {
 	mp.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
 	mp.mbi.On("RemoveFireflySubscription", mock.Anything, mock.Anything).Return(nil)
 
-	mp.multipartyManager.namespace.Contracts = core.MultipartyContracts{
-		Active: core.MultipartyContract{Index: 0},
+	mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
+		Active: &core.MultipartyContract{Index: 0},
 	}
 	mp.multipartyManager.config.Contracts = []Contract{{
 		FirstEvent: "0",
@@ -558,8 +537,8 @@ func TestTerminateContractError(t *testing.T) {
 		"address": "0x123",
 	}.String())
 
-	mp.multipartyManager.namespace.Contracts = core.MultipartyContracts{
-		Active: core.MultipartyContract{Index: 0, Location: location},
+	mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
+		Active: &core.MultipartyContract{Index: 0, Location: location},
 	}
 
 	err := mp.TerminateContract(context.Background(), location, &blockchain.Event{})
@@ -574,8 +553,8 @@ func TestTerminateContractWrongAddress(t *testing.T) {
 		"address": "0x123",
 	}.String())
 
-	mp.multipartyManager.namespace.Contracts = core.MultipartyContracts{
-		Active: core.MultipartyContract{Index: 0, Location: location},
+	mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
+		Active: &core.MultipartyContract{Index: 0, Location: location},
 	}
 
 	err := mp.TerminateContract(context.Background(), fftypes.JSONAnyPtr("{}"), &blockchain.Event{})
diff --git a/internal/multiparty/operations_test.go b/internal/multiparty/operations_test.go
index 185e712b4..c37772b27 100644
--- a/internal/multiparty/operations_test.go
+++ b/internal/multiparty/operations_test.go
@@ -178,7 +178,7 @@ func TestPrepareOperationBatchPinNotFound(t *testing.T) {
 func TestRunBatchPinV1(t *testing.T) {
 	mp := newTestMultipartyManager()
 	defer mp.cleanup(t)
-	mp.networkVersion = 1
+	mp.namespace.Contracts.Active.Info.Version = 1
 
 	op := &core.Operation{
 		Type:      core.OpTypeBlockchainPinBatch,
diff --git a/internal/namespace/manager_test.go b/internal/namespace/manager_test.go
index 95381ab2b..ae9e02a93 100644
--- a/internal/namespace/manager_test.go
+++ b/internal/namespace/manager_test.go
@@ -31,7 +31,6 @@ import (
 	"github.com/hyperledger/firefly/internal/database/difactory"
 	"github.com/hyperledger/firefly/internal/dataexchange/dxfactory"
 	"github.com/hyperledger/firefly/internal/identity/iifactory"
-	"github.com/hyperledger/firefly/internal/orchestrator"
 	"github.com/hyperledger/firefly/internal/sharedstorage/ssfactory"
 	"github.com/hyperledger/firefly/internal/tokens/tifactory"
 	"github.com/hyperledger/firefly/mocks/blockchainmocks"
@@ -40,7 +39,6 @@ import (
 	"github.com/hyperledger/firefly/mocks/eventsmocks"
 	"github.com/hyperledger/firefly/mocks/identitymocks"
 	"github.com/hyperledger/firefly/mocks/metricsmocks"
-	"github.com/hyperledger/firefly/mocks/multipartymocks"
 	"github.com/hyperledger/firefly/mocks/operationmocks"
 	"github.com/hyperledger/firefly/mocks/orchestratormocks"
 	"github.com/hyperledger/firefly/mocks/sharedstoragemocks"
@@ -117,7 +115,8 @@ func newTestNamespaceManager(resetConfig bool) *testNamespaceManager {
 		"tbd": {plugin: &identitymocks.Plugin{}},
 	}
 	nm.plugins.tokens = map[string]tokensPlugin{
-		"erc721": {plugin: nm.mti},
+		"erc721":  {plugin: nm.mti},
+		"erc1155": {plugin: nm.mti},
 	}
 	nm.plugins.events = map[string]eventsPlugin{
 		"websockets": {plugin: nm.mev},
@@ -138,12 +137,18 @@ func TestNewNamespaceManager(t *testing.T) {
 func TestInit(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
+	nm.metricsEnabled = true
 
 	mo := &orchestratormocks.Orchestrator{}
-	mmp := &multipartymocks.Manager{}
-	mo.On("Init", mock.Anything, mock.Anything).Return(nil)
-	mo.On("MultiParty").Return(mmp)
-	mmp.On("GetNetworkVersion").Return(2)
+	mo.On("Init", mock.Anything, mock.Anything).
+		Run(func(args mock.Arguments) {
+			nm.namespaces["default"].Contracts.Active = &core.MultipartyContract{
+				Info: core.MultipartyContractInfo{
+					Version: 2,
+				},
+			}
+		}).
+		Return(nil)
 	nm.utOrchestrator = mo
 
 	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
@@ -157,11 +162,13 @@ func TestInit(t *testing.T) {
 	nm.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
 	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
 
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.NoError(t, err)
 
 	assert.Equal(t, mo, nm.Orchestrator("default"))
 	assert.Nil(t, nm.Orchestrator("unknown"))
+	assert.Nil(t, nm.Orchestrator(core.LegacySystemNamespace))
 
 	mo.AssertExpectations(t)
 }
@@ -174,7 +181,8 @@ func TestInitDatabaseFail(t *testing.T) {
 
 	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(fmt.Errorf("pop"))
 
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.EqualError(t, err, "pop")
 }
 
@@ -188,7 +196,8 @@ func TestInitBlockchainFail(t *testing.T) {
 	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
 	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(fmt.Errorf("pop"))
 
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.EqualError(t, err, "pop")
 }
 
@@ -203,7 +212,8 @@ func TestInitDataExchangeFail(t *testing.T) {
 	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
 	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(fmt.Errorf("pop"))
 
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.EqualError(t, err, "pop")
 }
 
@@ -219,7 +229,8 @@ func TestInitSharedStorageFail(t *testing.T) {
 	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
 	nm.mps.On("Init", mock.Anything, mock.Anything).Return(fmt.Errorf("pop"))
 
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.EqualError(t, err, "pop")
 }
 
@@ -236,7 +247,8 @@ func TestInitTokensFail(t *testing.T) {
 	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
 	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("pop"))
 
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.EqualError(t, err, "pop")
 }
 
@@ -254,7 +266,8 @@ func TestInitEventsFail(t *testing.T) {
 	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
 	nm.mev.On("Init", mock.Anything, mock.Anything).Return(fmt.Errorf("pop"))
 
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.EqualError(t, err, "pop")
 }
 
@@ -273,7 +286,8 @@ func TestInitAuthFail(t *testing.T) {
 	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
 	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("pop"))
 
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.EqualError(t, err, "pop")
 }
 
@@ -281,40 +295,44 @@ func TestInitOrchestratorFail(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
 
+	nm.mdi.On("Capabilities").Return(&database.Capabilities{
+		Concurrency: true,
+	})
 	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
 	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
-	nm.mdi.On("SetHandler", "default", mock.Anything).Return()
-	nm.mdi.On("GetIdentities", mock.Anything, "default", mock.Anything).Return(nil, nil, fmt.Errorf("pop"))
 	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
-	nm.mbi.On("SetHandler", "default", mock.Anything).Return()
-	nm.mbi.On("SetOperationHandler", "default", mock.Anything).Return()
+	nm.mbi.On("GetAndConvertDeprecatedContractConfig", mock.Anything).Return(nil, "", fmt.Errorf("pop"))
 	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
 	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
-	nm.mps.On("SetHandler", "default", mock.Anything).Return()
 	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
 	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
 	nm.mdi.On("GetNamespace", mock.Anything, "default").Return(nil, nil)
 	nm.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
 	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
 
-	err := nm.Init(context.Background())
-	assert.EqualError(t, err, "pop")
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
+	assert.Regexp(t, "pop", err)
 }
 
 func TestInitVersion1(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
 
-	mmp := &multipartymocks.Manager{}
 	mo := &orchestratormocks.Orchestrator{}
-	mo.On("Init", mock.Anything, mock.Anything).Return(nil).Once()
+	mo.On("Init", mock.Anything, mock.Anything).
+		Run(func(args mock.Arguments) {
+			nm.namespaces["default"].Contracts.Active = &core.MultipartyContract{
+				Location: fftypes.JSONAnyPtr("{}"),
+				Info: core.MultipartyContractInfo{
+					Version: 1,
+				},
+			}
+		}).
+		Return(nil).Once()
 	mo.On("Init", mock.Anything, mock.Anything).Return(nil).Once()
 	nm.utOrchestrator = mo
 
-	mo.On("Init", mock.Anything, mock.Anything).Return(nil)
-	mo.On("MultiParty").Return(mmp)
-	mmp.On("GetNetworkVersion").Return(1)
-
 	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
 	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
 	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
@@ -328,7 +346,8 @@ func TestInitVersion1(t *testing.T) {
 	nm.mdi.On("GetNamespace", mock.Anything, core.LegacySystemNamespace).Return(nil, nil)
 	nm.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
 
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.NoError(t, err)
 
 	assert.Equal(t, mo, nm.Orchestrator("default"))
@@ -338,20 +357,270 @@ func TestInitVersion1(t *testing.T) {
 	mo.AssertExpectations(t)
 }
 
+func TestInitFFSystemWithTerminatedV1Contract(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+	nm.metricsEnabled = true
+
+	mo := &orchestratormocks.Orchestrator{}
+	mo.On("Init", mock.Anything, mock.Anything).
+		Run(func(args mock.Arguments) {
+			nm.namespaces["default"].Contracts = &core.MultipartyContracts{
+				Active: &core.MultipartyContract{
+					Info: core.MultipartyContractInfo{
+						Version: 2,
+					},
+				},
+				Terminated: []*core.MultipartyContract{{
+					Location: fftypes.JSONAnyPtr("{}"),
+					Info: core.MultipartyContractInfo{
+						Version: 1,
+					},
+				}},
+			}
+		}).
+		Return(nil)
+	nm.utOrchestrator = mo
+
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("GetNamespace", mock.Anything, "default").Return(nil, nil)
+	nm.mdi.On("GetNamespace", mock.Anything, core.LegacySystemNamespace).Return(nil, nil)
+	nm.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
+	assert.NoError(t, err)
+
+	assert.Equal(t, mo, nm.Orchestrator("default"))
+	assert.Nil(t, nm.Orchestrator("unknown"))
+
+	mo.AssertExpectations(t)
+}
+
+func TestLegacyNamespaceConflictingPlugins(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+	nm.metricsEnabled = true
+
+	viper.SetConfigType("yaml")
+	err := viper.ReadConfig(strings.NewReader(`
+  namespaces:
+    default: default
+    predefined:
+    - name: default
+      plugins: [ethereum, postgres, erc721, ipfs, ffdx]
+      multiparty:
+        enabled: true
+    - name: ns2
+      plugins: [ethereum, postgres, erc1155, ipfs, ffdx]
+      multiparty:
+        enabled: true
+  `))
+	assert.NoError(t, err)
+
+	mo := &orchestratormocks.Orchestrator{}
+	mo.On("Init", mock.Anything, mock.Anything).
+		Run(func(args mock.Arguments) {
+			nm.namespaces["default"].Contracts = &core.MultipartyContracts{
+				Active: &core.MultipartyContract{
+					Location: fftypes.JSONAnyPtr("{}"),
+					Info: core.MultipartyContractInfo{
+						Version: 1,
+					},
+				},
+			}
+			nm.namespaces["ns2"].Contracts = &core.MultipartyContracts{
+				Active: &core.MultipartyContract{
+					Location: fftypes.JSONAnyPtr("{}"),
+					Info: core.MultipartyContractInfo{
+						Version: 1,
+					},
+				},
+			}
+		}).
+		Return(nil)
+	nm.utOrchestrator = mo
+
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("GetNamespace", mock.Anything, "default").Return(nil, nil)
+	nm.mdi.On("GetNamespace", mock.Anything, "ns2").Return(nil, nil)
+	nm.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
+	assert.Regexp(t, "FF10421", err)
+
+	assert.Equal(t, mo, nm.Orchestrator("default"))
+	assert.Nil(t, nm.Orchestrator("unknown"))
+
+	mo.AssertExpectations(t)
+}
+
+func TestLegacyNamespaceConflictingPluginsTooManyPlugins(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+	nm.metricsEnabled = true
+
+	viper.SetConfigType("yaml")
+	err := viper.ReadConfig(strings.NewReader(`
+  namespaces:
+    default: default
+    predefined:
+    - name: default
+      plugins: [ethereum, postgres, erc721, ipfs, ffdx]
+      multiparty:
+        enabled: true
+    - name: ns2
+      plugins: [ethereum, postgres, erc1155, erc721, ipfs, ffdx]
+      multiparty:
+        enabled: true
+  `))
+	assert.NoError(t, err)
+
+	mo := &orchestratormocks.Orchestrator{}
+	mo.On("Init", mock.Anything, mock.Anything).
+		Run(func(args mock.Arguments) {
+			nm.namespaces["default"].Contracts = &core.MultipartyContracts{
+				Active: &core.MultipartyContract{
+					Location: fftypes.JSONAnyPtr("{}"),
+					Info: core.MultipartyContractInfo{
+						Version: 1,
+					},
+				},
+			}
+			nm.namespaces["ns2"].Contracts = &core.MultipartyContracts{
+				Active: &core.MultipartyContract{
+					Location: fftypes.JSONAnyPtr("{}"),
+					Info: core.MultipartyContractInfo{
+						Version: 1,
+					},
+				},
+			}
+		}).
+		Return(nil)
+	nm.utOrchestrator = mo
+
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("GetNamespace", mock.Anything, "default").Return(nil, nil)
+	nm.mdi.On("GetNamespace", mock.Anything, "ns2").Return(nil, nil)
+	nm.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
+	assert.Regexp(t, "FF10421", err)
+
+	assert.Equal(t, mo, nm.Orchestrator("default"))
+	assert.Nil(t, nm.Orchestrator("unknown"))
+
+	mo.AssertExpectations(t)
+}
+
+func TestLegacyNamespaceMatchingPlugins(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+	nm.metricsEnabled = true
+
+	viper.SetConfigType("yaml")
+	err := viper.ReadConfig(strings.NewReader(`
+  namespaces:
+    default: default
+    predefined:
+    - name: default
+      plugins: [ethereum, postgres, erc721, ipfs, ffdx]
+      multiparty:
+        enabled: true
+    - name: ns2
+      plugins: [ethereum, postgres, erc721, ipfs, ffdx]
+      multiparty:
+        enabled: true
+  `))
+	assert.NoError(t, err)
+
+	mo := &orchestratormocks.Orchestrator{}
+	mo.On("Init", mock.Anything, mock.Anything).
+		Run(func(args mock.Arguments) {
+			nm.namespaces["default"].Contracts = &core.MultipartyContracts{
+				Active: &core.MultipartyContract{
+					Location: fftypes.JSONAnyPtr("{}"),
+					Info: core.MultipartyContractInfo{
+						Version: 1,
+					},
+				},
+			}
+			nm.namespaces["ns2"].Contracts = &core.MultipartyContracts{
+				Active: &core.MultipartyContract{
+					Location: fftypes.JSONAnyPtr("{}"),
+					Info: core.MultipartyContractInfo{
+						Version: 1,
+					},
+				},
+			}
+		}).
+		Return(nil)
+	nm.utOrchestrator = mo
+
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("GetNamespace", mock.Anything, "default").Return(nil, nil)
+	nm.mdi.On("GetNamespace", mock.Anything, "ns2").Return(nil, nil)
+	nm.mdi.On("GetNamespace", mock.Anything, core.LegacySystemNamespace).Return(nil, nil)
+	nm.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
+	assert.NoError(t, err)
+
+	assert.Equal(t, mo, nm.Orchestrator("default"))
+	assert.Nil(t, nm.Orchestrator("unknown"))
+
+	mo.AssertExpectations(t)
+}
+
 func TestInitVersion1Fail(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
 
 	mo := &orchestratormocks.Orchestrator{}
-	mmp := &multipartymocks.Manager{}
-	mo.On("Init", mock.Anything, mock.Anything).Return(nil).Once()
+	mo.On("Init", mock.Anything, mock.Anything).
+		Run(func(args mock.Arguments) {
+			nm.namespaces["default"].Contracts.Active = &core.MultipartyContract{
+				Location: fftypes.JSONAnyPtr("{}"),
+				Info: core.MultipartyContractInfo{
+					Version: 1,
+				},
+			}
+		}).
+		Return(nil).Once()
 	mo.On("Init", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")).Once()
 	nm.utOrchestrator = mo
 
-	mo.On("Init", mock.Anything, mock.Anything).Return(nil)
-	mo.On("MultiParty").Return(mmp)
-	mmp.On("GetNetworkVersion").Return(1)
-
 	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
 	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
 	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
@@ -365,7 +634,8 @@ func TestInitVersion1Fail(t *testing.T) {
 	nm.mdi.On("GetNamespace", mock.Anything, core.LegacySystemNamespace).Return(nil, nil)
 	nm.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
 
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.EqualError(t, err, "pop")
 
 	mo.AssertExpectations(t)
@@ -376,16 +646,15 @@ func TestInitNamespaceQueryFail(t *testing.T) {
 	defer nm.cleanup(t)
 
 	ns := &namespace{
-		plugins: orchestrator.Plugins{
-			Database: orchestrator.DatabasePlugin{
-				Plugin: nm.mdi,
-			},
+		Namespace: core.Namespace{
+			LocalName: "default",
 		},
+		plugins: []string{"postgres"},
 	}
 
 	nm.mdi.On("GetNamespace", mock.Anything, "default").Return(nil, fmt.Errorf("pop"))
 
-	err := nm.initNamespace("default", ns)
+	err := nm.initNamespace(ns)
 	assert.EqualError(t, err, "pop")
 }
 
@@ -394,17 +663,16 @@ func TestInitNamespaceExistingUpsertFail(t *testing.T) {
 	defer nm.cleanup(t)
 
 	ns := &namespace{
-		plugins: orchestrator.Plugins{
-			Database: orchestrator.DatabasePlugin{
-				Plugin: nm.mdi,
-			},
+		Namespace: core.Namespace{
+			LocalName: "default",
 		},
+		plugins: []string{"postgres"},
 	}
 
 	nm.mdi.On("GetNamespace", mock.Anything, "default").Return(&core.Namespace{}, nil)
 	nm.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(fmt.Errorf("pop"))
 
-	err := nm.initNamespace("default", ns)
+	err := nm.initNamespace(ns)
 	assert.EqualError(t, err, "pop")
 }
 
@@ -459,7 +727,8 @@ func TestDatabasePluginBadName(t *testing.T) {
 	config.Set("plugins.database", []fftypes.JSONObject{{}})
 	databaseConfig.AddKnownKey(coreconfig.PluginConfigName, "wrong////")
 	databaseConfig.AddKnownKey(coreconfig.PluginConfigType, "postgres")
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.Error(t, err)
 }
 
@@ -492,7 +761,8 @@ func TestIdentityPluginNoType(t *testing.T) {
 	iifactory.InitConfig(identityConfig)
 	identityConfig.AddKnownKey(coreconfig.PluginConfigName, "flapflip")
 	config.Set("plugins.identity", []fftypes.JSONObject{{}})
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF10386.*type", err)
 }
 
@@ -556,7 +826,8 @@ func TestBlockchainPluginBadType(t *testing.T) {
 	config.Set("plugins.blockchain", []fftypes.JSONObject{{}})
 	blockchainConfig.AddKnownKey(coreconfig.PluginConfigName, "flapflip")
 	blockchainConfig.AddKnownKey(coreconfig.PluginConfigType, "wrong//")
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.Error(t, err)
 }
 
@@ -609,7 +880,8 @@ func TestSharedStoragePluginBadType(t *testing.T) {
 	config.Set("plugins.sharedstorage", []fftypes.JSONObject{{}})
 	sharedstorageConfig.AddKnownKey(coreconfig.PluginConfigName, "flapflip")
 	sharedstorageConfig.AddKnownKey(coreconfig.PluginConfigType, "wrong//")
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.Error(t, err)
 }
 
@@ -662,7 +934,8 @@ func TestDataExchangePluginBadType(t *testing.T) {
 	config.Set("plugins.dataexchange", []fftypes.JSONObject{{}})
 	dataexchangeConfig.AddKnownKey(coreconfig.PluginConfigName, "flapflip")
 	dataexchangeConfig.AddKnownKey(coreconfig.PluginConfigType, "wrong//")
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.Error(t, err)
 }
 
@@ -792,7 +1065,8 @@ func TestTokensPluginBadType(t *testing.T) {
 	config.Set("plugins.tokens", []fftypes.JSONObject{{}})
 	tokensConfig.AddKnownKey(coreconfig.PluginConfigName, "erc20_erc721")
 	tokensConfig.AddKnownKey(coreconfig.PluginConfigType, "wrong")
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.Error(t, err)
 }
 
@@ -837,7 +1111,8 @@ func TestAuthPluginBadType(t *testing.T) {
 	config.Set("plugins.auth", []fftypes.JSONObject{{}})
 	authConfig.AddKnownKey(coreconfig.PluginConfigName, "basicauth")
 	authConfig.AddKnownKey(coreconfig.PluginConfigType, "wrong")
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.Error(t, err)
 }
 
@@ -858,7 +1133,8 @@ func TestEventsPluginBadType(t *testing.T) {
 	defer nm.cleanup(t)
 	nm.plugins.events = nil
 	config.Set(coreconfig.EventTransportsEnabled, []string{"!unknown!"})
-	err := nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err := nm.Init(ctx, cancelCtx)
 	assert.Error(t, err)
 }
 
@@ -886,7 +1162,8 @@ func TestInitBadNamespace(t *testing.T) {
     `))
 	assert.NoError(t, err)
 
-	err = nm.Init(context.Background())
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF00140", err)
 }
 
@@ -996,7 +1273,17 @@ func TestLoadNamespacesNonMultipartyNoDatabase(t *testing.T) {
   `))
 	assert.NoError(t, err)
 
-	err = nm.loadNamespaces(context.Background())
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF10392", err)
 }
 
@@ -1010,13 +1297,23 @@ func TestLoadNamespacesMultipartyUnknownPlugin(t *testing.T) {
     default: ns1
     predefined:
     - name: ns1
-      plugins: [bad]
+      plugins: [basicauth, bad]
       multiparty:
         enabled: true
   `))
 	assert.NoError(t, err)
 
-	err = nm.loadNamespaces(context.Background())
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF10390.*unknown", err)
 }
 
@@ -1036,7 +1333,17 @@ func TestLoadNamespacesMultipartyMultipleBlockchains(t *testing.T) {
   `))
 	assert.NoError(t, err)
 
-	err = nm.loadNamespaces(context.Background())
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF10394.*blockchain", err)
 }
 
@@ -1056,7 +1363,17 @@ func TestLoadNamespacesMultipartyMultipleDX(t *testing.T) {
   `))
 	assert.NoError(t, err)
 
-	err = nm.loadNamespaces(context.Background())
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF10394.*dataexchange", err)
 }
 
@@ -1076,7 +1393,17 @@ func TestLoadNamespacesMultipartyMultipleSS(t *testing.T) {
   `))
 	assert.NoError(t, err)
 
-	err = nm.loadNamespaces(context.Background())
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF10394.*sharedstorage", err)
 }
 
@@ -1096,11 +1423,21 @@ func TestLoadNamespacesMultipartyMultipleDB(t *testing.T) {
   `))
 	assert.NoError(t, err)
 
-	err = nm.loadNamespaces(context.Background())
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF10394.*database", err)
 }
 
-func TestLoadNamespacesMultipartyWithAuth(t *testing.T) {
+func TestInitNamespacesMultipartyWithAuth(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
 
@@ -1188,7 +1525,17 @@ func TestLoadNamespacesNonMultipartyMultipleDB(t *testing.T) {
   `))
 	assert.NoError(t, err)
 
-	err = nm.loadNamespaces(context.Background())
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF10394.*database", err)
 }
 
@@ -1206,7 +1553,17 @@ func TestLoadNamespacesNonMultipartyMultipleBlockchains(t *testing.T) {
   `))
 	assert.NoError(t, err)
 
-	err = nm.loadNamespaces(context.Background())
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF10394.*blockchain", err)
 }
 
@@ -1226,7 +1583,17 @@ func TestLoadNamespacesMultipartyMissingPlugins(t *testing.T) {
   `))
 	assert.NoError(t, err)
 
-	err = nm.loadNamespaces(context.Background())
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF10391", err)
 }
 
@@ -1244,7 +1611,17 @@ func TestLoadNamespacesNonMultipartyWithDX(t *testing.T) {
   `))
 	assert.NoError(t, err)
 
-	err = nm.loadNamespaces(context.Background())
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF10393", err)
 }
 
@@ -1262,7 +1639,17 @@ func TestLoadNamespacesNonMultipartyWithSharedStorage(t *testing.T) {
   `))
 	assert.NoError(t, err)
 
-	err = nm.loadNamespaces(context.Background())
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF10393", err)
 }
 
@@ -1276,11 +1663,21 @@ func TestLoadNamespacesNonMultipartyUnknownPlugin(t *testing.T) {
     default: ns1
     predefined:
     - name: ns1
-      plugins: [bad]
+      plugins: [basicauth, erc721, bad]
   `))
 	assert.NoError(t, err)
 
-	err = nm.loadNamespaces(context.Background())
+	nm.mdi.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mdi.On("SetHandler", database.GlobalHandler, mock.Anything).Return()
+	nm.mbi.On("Init", mock.Anything, mock.Anything, nm.mmi).Return(nil)
+	nm.mdx.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mps.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.mti.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+	nm.mev.On("Init", mock.Anything, mock.Anything).Return(nil)
+	nm.auth.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+	ctx, cancelCtx := context.WithCancel(context.Background())
+	err = nm.Init(ctx, cancelCtx)
 	assert.Regexp(t, "FF10390.*unknown", err)
 }
 
@@ -1340,6 +1737,8 @@ func TestStartBlockchainFail(t *testing.T) {
 	err := nm.Start()
 	assert.EqualError(t, err, "pop")
 
+	mo.AssertExpectations(t)
+
 }
 
 func TestStartDataExchangeFail(t *testing.T) {
@@ -1360,6 +1759,8 @@ func TestStartDataExchangeFail(t *testing.T) {
 	err := nm.Start()
 	assert.EqualError(t, err, "pop")
 
+	mo.AssertExpectations(t)
+
 }
 
 func TestStartTokensFail(t *testing.T) {
@@ -1400,7 +1801,7 @@ func TestStartOrchestratorFail(t *testing.T) {
 	mo.AssertExpectations(t)
 }
 
-func TestStartStop(t *testing.T) {
+func TestWaitStop(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
 
@@ -1408,40 +1809,30 @@ func TestStartStop(t *testing.T) {
 	nm.namespaces = map[string]*namespace{
 		"ns": {orchestrator: mo},
 	}
-	nm.plugins.blockchain = nil
-	nm.plugins.dataexchange = nil
-	nm.plugins.tokens = nil
-
-	mo.On("Start", mock.Anything).Return(nil)
+	mae := nm.adminEvents.(*spieventsmocks.Manager)
 
-	err := nm.Start()
-	assert.NoError(t, err)
-	assert.Len(t, nm.namespaces, 1)
+	mo.On("WaitStop").Return()
+	mae.On("WaitStop").Return()
 
-	onStop := mo.Calls[0].Arguments[0].(func())
-	onStop()
-	assert.Len(t, nm.namespaces, 0)
+	nm.WaitStop()
 
 	mo.AssertExpectations(t)
+	mae.AssertExpectations(t)
 }
 
-func TestWaitStop(t *testing.T) {
+func TestReset(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
 
-	mo := &orchestratormocks.Orchestrator{}
-	nm.namespaces = map[string]*namespace{
-		"ns": {orchestrator: mo},
-	}
-	mae := nm.adminEvents.(*spieventsmocks.Manager)
-
-	mo.On("WaitStop").Return()
-	mae.On("WaitStop").Return()
+	parentCtx, parentCancel := context.WithCancel(context.Background())
+	nm.namespaceManager.ctx = parentCtx
+	nm.namespaceManager.cancelCtx = parentCancel
 
-	nm.WaitStop()
+	childCtx, childCancel := context.WithCancel(context.Background())
+	nm.Reset(childCtx)
+	childCancel()
 
-	mo.AssertExpectations(t)
-	mae.AssertExpectations(t)
+	<-parentCtx.Done()
 }
 
 func TestLoadMetrics(t *testing.T) {
@@ -1469,15 +1860,10 @@ func TestGetNamespaces(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
 
-	mo := &orchestratormocks.Orchestrator{}
 	nm.namespaces = map[string]*namespace{
-		"default": {
-			orchestrator: mo,
-		},
+		"default": {},
 	}
 
-	mo.On("GetNamespace", context.Background()).Return(&core.Namespace{})
-
 	results, err := nm.GetNamespaces(context.Background())
 	assert.Nil(t, err)
 	assert.Len(t, results, 1)
@@ -1590,14 +1976,34 @@ func TestResolveOperationByNamespacedIDNoOrchestrator(t *testing.T) {
 func TestAuthorize(t *testing.T) {
 	nm := newTestNamespaceManager(true)
 	defer nm.cleanup(t)
-	or := &orchestratormocks.Orchestrator{}
-	or.On("Authorize", mock.Anything, mock.Anything).Return(nil)
+	mo := &orchestratormocks.Orchestrator{}
+	mo.On("Authorize", mock.Anything, mock.Anything).Return(nil)
 	nm.namespaces["ns1"] = &namespace{
-		orchestrator: or,
+		orchestrator: mo,
 	}
-	nm.utOrchestrator = or
+	nm.utOrchestrator = mo
 	err := nm.Authorize(context.Background(), &fftypes.AuthReq{
 		Namespace: "ns1",
 	})
 	assert.NoError(t, err)
+
+	mo.AssertExpectations(t)
+}
+
+func TestValidateNonMultipartyConfig(t *testing.T) {
+	nm := newTestNamespaceManager(true)
+	defer nm.cleanup(t)
+
+	viper.SetConfigType("yaml")
+	err := viper.ReadConfig(strings.NewReader(`
+    namespaces:
+      default: ns1
+      predefined:
+      - name: ns1
+        plugins: [postgres, erc721]
+    `))
+	assert.NoError(t, err)
+
+	_, err = nm.validateNonMultipartyConfig(nm.ctx, "ns1", []string{"postgres", "erc721"})
+	assert.NoError(t, err)
 }
diff --git a/internal/orchestrator/bound_callbacks_test.go b/internal/orchestrator/bound_callbacks_test.go
index fddc14db2..2697f2ad1 100644
--- a/internal/orchestrator/bound_callbacks_test.go
+++ b/internal/orchestrator/bound_callbacks_test.go
@@ -27,11 +27,8 @@ import (
 	"github.com/hyperledger/firefly/mocks/eventmocks"
 	"github.com/hyperledger/firefly/mocks/operationmocks"
 	"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/dataexchange"
-	"github.com/hyperledger/firefly/pkg/tokens"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 )
@@ -40,29 +37,14 @@ func TestBoundCallbacks(t *testing.T) {
 	mei := &eventmocks.EventManager{}
 	mbi := &blockchainmocks.Plugin{}
 	mdx := &dataexchangemocks.Plugin{}
-	mti := &tokenmocks.Plugin{}
 	mss := &sharedstoragemocks.Plugin{}
 	mom := &operationmocks.Manager{}
 	bc := boundCallbacks{dx: mdx, ei: mei, ss: mss, om: mom}
 
 	info := fftypes.JSONObject{"hello": "world"}
-	batch := &blockchain.BatchPin{TransactionID: fftypes.NewUUID()}
-	pool := &tokens.TokenPool{}
-	transfer := &tokens.TokenTransfer{}
-	approval := &tokens.TokenApproval{}
-	location := fftypes.JSONAnyPtr("{}")
-	event := &blockchain.Event{}
 	hash := fftypes.NewRandB32()
 	opID := fftypes.NewUUID()
 
-	mei.On("BatchPinComplete", batch, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}).Return(fmt.Errorf("pop"))
-	err := bc.BatchPinComplete(batch, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress})
-	assert.EqualError(t, err, "pop")
-
-	mei.On("BlockchainNetworkAction", "terminate", location, event, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress}).Return(fmt.Errorf("pop"))
-	err = bc.BlockchainNetworkAction("terminate", location, event, &core.VerifierRef{Value: "0x12345", Type: core.VerifierTypeEthAddress})
-	assert.EqualError(t, err, "pop")
-
 	nsOpID := "ns1:" + opID.String()
 	mom.On("SubmitOperationUpdate", mock.Anything, &operations.OperationUpdate{
 		NamespacedOpID: nsOpID,
@@ -84,24 +66,8 @@ func TestBoundCallbacks(t *testing.T) {
 	mde.On("Type").Return(dataexchange.DXEventTypeMessageReceived).Once()
 	bc.DXEvent(mde)
 
-	mei.On("TokenPoolCreated", mti, pool).Return(fmt.Errorf("pop"))
-	err = bc.TokenPoolCreated(mti, pool)
-	assert.EqualError(t, err, "pop")
-
-	mei.On("TokensTransferred", mti, transfer).Return(fmt.Errorf("pop"))
-	err = bc.TokensTransferred(mti, transfer)
-	assert.EqualError(t, err, "pop")
-
-	mei.On("TokensApproved", mti, approval).Return(fmt.Errorf("pop"))
-	err = bc.TokensApproved(mti, approval)
-	assert.EqualError(t, err, "pop")
-
-	mei.On("BlockchainEvent", mock.AnythingOfType("*blockchain.EventWithSubscription")).Return(fmt.Errorf("pop"))
-	err = bc.BlockchainEvent(&blockchain.EventWithSubscription{})
-	assert.EqualError(t, err, "pop")
-
 	mei.On("SharedStorageBatchDownloaded", mss, "payload1", []byte(`{}`)).Return(nil, fmt.Errorf("pop"))
-	_, err = bc.SharedStorageBatchDownloaded("payload1", []byte(`{}`))
+	_, err := bc.SharedStorageBatchDownloaded("payload1", []byte(`{}`))
 	assert.EqualError(t, err, "pop")
 
 	mei.On("SharedStorageBlobDownloaded", mss, *hash, int64(12345), "payload1").Return()
diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go
index 46c03eff2..33c697b47 100644
--- a/internal/orchestrator/orchestrator_test.go
+++ b/internal/orchestrator/orchestrator_test.go
@@ -157,14 +157,24 @@ func newTestOrchestrator() *testOrchestrator {
 	tor.orchestrator.defhandler = tor.mdh
 	tor.orchestrator.defsender = tor.mds
 	tor.orchestrator.config.Multiparty.Enabled = true
-	tor.orchestrator.plugins.Blockchain.Plugin = tor.mbi
-	tor.orchestrator.plugins.SharedStorage.Plugin = tor.mps
-	tor.orchestrator.plugins.DataExchange.Plugin = tor.mdx
-	tor.orchestrator.plugins.Database.Plugin = tor.mdi
-	tor.orchestrator.plugins.Tokens = []TokensPlugin{{
-		Name:   "token",
-		Plugin: tor.mti,
-	}}
+	tor.orchestrator.plugins = &Plugins{
+		Blockchain: BlockchainPlugin{
+			Plugin: tor.mbi,
+		},
+		SharedStorage: SharedStoragePlugin{
+			Plugin: tor.mps,
+		},
+		DataExchange: DataExchangePlugin{
+			Plugin: tor.mdx,
+		},
+		Database: DatabasePlugin{
+			Plugin: tor.mdi,
+		},
+		Tokens: []TokensPlugin{{
+			Name:   "token",
+			Plugin: tor.mti,
+		}},
+	}
 	tor.mdi.On("Name").Return("mock-di").Maybe()
 	tor.mem.On("Name").Return("mock-ei").Maybe()
 	tor.mps.On("Name").Return("mock-ps").Maybe()
@@ -184,7 +194,7 @@ func TestNewOrchestrator(t *testing.T) {
 	or := NewOrchestrator(
 		&core.Namespace{LocalName: "ns1", RemoteName: "ns1"},
 		Config{},
-		Plugins{},
+		&Plugins{},
 		&metricsmocks.Manager{},
 	)
 	assert.NotNil(t, or)
@@ -203,7 +213,8 @@ func TestInitOK(t *testing.T) {
 	or.mps.On("SetHandler", "ns2", mock.Anything).Return()
 	or.mti.On("SetHandler", "ns", mock.Anything).Return(nil)
 	or.mti.On("SetOperationHandler", "ns", mock.Anything).Return(nil)
-	err := or.Init(or.ctx)
+	or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil)
+	err := or.Init(or.ctx, or.cancelCtx)
 	assert.NoError(t, err)
 
 	assert.Equal(t, or.mba, or.BatchManager())
@@ -230,7 +241,7 @@ func TestInitTokenListenerFail(t *testing.T) {
 	or.mdx.On("SetNodes", mock.Anything).Return()
 	or.mps.On("SetHandler", "ns", mock.Anything).Return()
 	or.mti.On("SetHandler", "ns", mock.Anything).Return(fmt.Errorf("pop"))
-	err := or.Init(or.ctx)
+	err := or.initHandlers(context.Background())
 	assert.EqualError(t, err, "pop")
 }
 
@@ -243,7 +254,7 @@ func TestInitDataexchangeNodesFail(t *testing.T) {
 	or.mps.On("SetHandler", "ns", mock.Anything).Return()
 	or.mdi.On("GetIdentities", mock.Anything, "ns", mock.Anything).Return(nil, nil, fmt.Errorf("pop"))
 	ctx := context.Background()
-	err := or.initPlugins(ctx)
+	err := or.initHandlers(ctx)
 	assert.EqualError(t, err, "pop")
 }
 
@@ -252,6 +263,7 @@ func TestInitMessagingComponentFail(t *testing.T) {
 	defer or.cleanup(t)
 	or.plugins.Database.Plugin = nil
 	or.messaging = nil
+	or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil)
 	err := or.initComponents(context.Background())
 	assert.Regexp(t, "FF10128", err)
 }
@@ -261,6 +273,7 @@ func TestInitEventsComponentFail(t *testing.T) {
 	defer or.cleanup(t)
 	or.plugins.Database.Plugin = nil
 	or.events = nil
+	or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil)
 	err := or.initComponents(context.Background())
 	assert.Regexp(t, "FF10128", err)
 }
@@ -270,6 +283,7 @@ func TestInitNetworkMapComponentFail(t *testing.T) {
 	defer or.cleanup(t)
 	or.plugins.Database.Plugin = nil
 	or.networkmap = nil
+	or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil)
 	err := or.initComponents(context.Background())
 	assert.Regexp(t, "FF10128", err)
 }
@@ -283,11 +297,20 @@ func TestInitMultipartyComponentFail(t *testing.T) {
 	assert.Regexp(t, "FF10128", err)
 }
 
+func TestInitMultipartyComponentConfigureFail(t *testing.T) {
+	or := newTestOrchestrator()
+	defer or.cleanup(t)
+	or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(fmt.Errorf("pop"))
+	err := or.initComponents(context.Background())
+	assert.EqualError(t, err, "pop")
+}
+
 func TestInitSharedStorageDownloadComponentFail(t *testing.T) {
 	or := newTestOrchestrator()
 	defer or.cleanup(t)
 	or.plugins.Database.Plugin = nil
 	or.sharedDownload = nil
+	or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil)
 	err := or.initComponents(context.Background())
 	assert.Regexp(t, "FF10128", err)
 }
@@ -297,6 +320,7 @@ func TestInitBatchComponentFail(t *testing.T) {
 	defer or.cleanup(t)
 	or.plugins.Database.Plugin = nil
 	or.batch = nil
+	or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil)
 	err := or.initComponents(context.Background())
 	assert.Regexp(t, "FF10128", err)
 }
@@ -306,6 +330,7 @@ func TestInitBroadcastComponentFail(t *testing.T) {
 	defer or.cleanup(t)
 	or.plugins.Database.Plugin = nil
 	or.broadcast = nil
+	or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil)
 	err := or.initComponents(context.Background())
 	assert.Regexp(t, "FF10128", err)
 }
@@ -315,6 +340,7 @@ func TestInitDefSenderComponentFail(t *testing.T) {
 	defer or.cleanup(t)
 	or.data = nil
 	or.defsender = nil
+	or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil)
 	err := or.initManagers(context.Background())
 	assert.Regexp(t, "FF10128", err)
 }
@@ -333,6 +359,7 @@ func TestInitIdentityComponentFail(t *testing.T) {
 	defer or.cleanup(t)
 	or.plugins.Database.Plugin = nil
 	or.identity = nil
+	or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil)
 	err := or.initComponents(context.Background())
 	assert.Regexp(t, "FF10128", err)
 }
@@ -342,6 +369,7 @@ func TestInitAssetsComponentFail(t *testing.T) {
 	defer or.cleanup(t)
 	or.plugins.Database.Plugin = nil
 	or.assets = nil
+	or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil)
 	err := or.initComponents(context.Background())
 	assert.Regexp(t, "FF10128", err)
 }
@@ -351,6 +379,7 @@ func TestInitContractsComponentFail(t *testing.T) {
 	defer or.cleanup(t)
 	or.plugins.Database.Plugin = nil
 	or.contracts = nil
+	or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil)
 	err := or.initComponents(context.Background())
 	assert.Regexp(t, "FF10128", err)
 }
@@ -370,19 +399,8 @@ func TestStartBatchFail(t *testing.T) {
 	or := newTestOrchestrator()
 	defer or.cleanup(t)
 	or.mdm.On("Start").Return(nil)
-	or.mmp.On("ConfigureContract", mock.Anything).Return(nil)
 	or.mba.On("Start").Return(fmt.Errorf("pop"))
-	err := or.Start(func() {})
-	assert.EqualError(t, err, "pop")
-}
-
-func TestStartBlockchainsConfigureFail(t *testing.T) {
-	coreconfig.Reset()
-	or := newTestOrchestrator()
-	defer or.cleanup(t)
-	or.mdm.On("Start").Return(nil)
-	or.mmp.On("ConfigureContract", mock.Anything).Return(fmt.Errorf("pop"))
-	err := or.Start(func() {})
+	err := or.Start()
 	assert.EqualError(t, err, "pop")
 }
 
@@ -390,7 +408,6 @@ func TestStartStopOk(t *testing.T) {
 	coreconfig.Reset()
 	or := newTestOrchestrator()
 	defer or.cleanup(t)
-	or.mmp.On("ConfigureContract", mock.Anything).Return(nil)
 	or.mdm.On("Start").Return(nil)
 	or.mba.On("Start").Return(nil)
 	or.mem.On("Start").Return(nil)
@@ -403,7 +420,7 @@ func TestStartStopOk(t *testing.T) {
 	or.msd.On("WaitStop").Return(nil)
 	or.mom.On("WaitStop").Return(nil)
 	or.mem.On("WaitStop").Return(nil)
-	err := or.Start(func() {})
+	err := or.Start()
 	assert.NoError(t, err)
 	or.WaitStop()
 	or.WaitStop() // swallows dups
@@ -435,14 +452,6 @@ func TestNetworkActionNonMultiparty(t *testing.T) {
 	assert.Regexp(t, "FF10414", err)
 }
 
-func TestStop(t *testing.T) {
-	or := newTestOrchestrator()
-	called := false
-	or.onStop = func() { called = true }
-	or.stop()
-	assert.True(t, called)
-}
-
 func TestAuthorize(t *testing.T) {
 	or := newTestOrchestrator()
 	auth := &authmocks.Plugin{}
diff --git a/pkg/core/namespace_test.go b/pkg/core/namespace_test.go
index 6b5ce5aeb..7b1bd614a 100644
--- a/pkg/core/namespace_test.go
+++ b/pkg/core/namespace_test.go
@@ -35,7 +35,7 @@ func TestNamespace(t *testing.T) {
 
 func TestMultipartyContractsDatabaseSerialization(t *testing.T) {
 	contracts1 := &MultipartyContracts{
-		Active: MultipartyContract{
+		Active: &MultipartyContract{
 			Index:      1,
 			FirstEvent: "oldest",
 			Location: fftypes.JSONAnyPtr(fftypes.JSONObject{
@@ -45,7 +45,7 @@ func TestMultipartyContractsDatabaseSerialization(t *testing.T) {
 				Subscription: "1234",
 			},
 		},
-		Terminated: []MultipartyContract{
+		Terminated: []*MultipartyContract{
 			{
 				Index:      0,
 				FirstEvent: "oldest",
diff --git a/test/data/contracts/firefly/Firefly.json b/test/data/contracts/firefly/Firefly.json
new file mode 100644
index 000000000..2bb754a7f
--- /dev/null
+++ b/test/data/contracts/firefly/Firefly.json
@@ -0,0 +1,119 @@
+{
+  "_format": "hh-sol-artifact-1",
+  "contractName": "Firefly",
+  "sourceName": "contracts/Firefly.sol",
+  "abi": [
+    {
+      "anonymous": false,
+      "inputs": [
+        {
+          "indexed": false,
+          "internalType": "address",
+          "name": "author",
+          "type": "address"
+        },
+        {
+          "indexed": false,
+          "internalType": "uint256",
+          "name": "timestamp",
+          "type": "uint256"
+        },
+        {
+          "indexed": false,
+          "internalType": "string",
+          "name": "action",
+          "type": "string"
+        },
+        {
+          "indexed": false,
+          "internalType": "bytes32",
+          "name": "uuids",
+          "type": "bytes32"
+        },
+        {
+          "indexed": false,
+          "internalType": "bytes32",
+          "name": "batchHash",
+          "type": "bytes32"
+        },
+        {
+          "indexed": false,
+          "internalType": "string",
+          "name": "payloadRef",
+          "type": "string"
+        },
+        {
+          "indexed": false,
+          "internalType": "bytes32[]",
+          "name": "contexts",
+          "type": "bytes32[]"
+        }
+      ],
+      "name": "BatchPin",
+      "type": "event"
+    },
+    {
+      "inputs": [
+        {
+          "internalType": "string",
+          "name": "action",
+          "type": "string"
+        },
+        {
+          "internalType": "string",
+          "name": "payload",
+          "type": "string"
+        }
+      ],
+      "name": "networkAction",
+      "outputs": [],
+      "stateMutability": "nonpayable",
+      "type": "function"
+    },
+    {
+      "inputs": [],
+      "name": "networkVersion",
+      "outputs": [
+        {
+          "internalType": "uint8",
+          "name": "",
+          "type": "uint8"
+        }
+      ],
+      "stateMutability": "pure",
+      "type": "function"
+    },
+    {
+      "inputs": [
+        {
+          "internalType": "bytes32",
+          "name": "uuids",
+          "type": "bytes32"
+        },
+        {
+          "internalType": "bytes32",
+          "name": "batchHash",
+          "type": "bytes32"
+        },
+        {
+          "internalType": "string",
+          "name": "payloadRef",
+          "type": "string"
+        },
+        {
+          "internalType": "bytes32[]",
+          "name": "contexts",
+          "type": "bytes32[]"
+        }
+      ],
+      "name": "pinBatch",
+      "outputs": [],
+      "stateMutability": "nonpayable",
+      "type": "function"
+    }
+  ],
+  "bytecode": "0x608060405234801561001057600080fd5b506107f4806100206000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c806348ce1dcc1461004657806397eacfe614610062578063fb9066c114610080575b600080fd5b610060600480360381019061005b919061024a565b61009c565b005b61006a6100e3565b604051610077919061057b565b60405180910390f35b61009a600480360381019061009591906102dd565b6100ec565b005b7f805721bc246bccc732581be0c0aa2dd8f7ec93e97ba4b307be84428c98b0a12f3342868686866040516100d5969594939291906104f9565b60405180910390a150505050565b60006002905090565b60607f805721bc246bccc732581be0c0aa2dd8f7ec93e97ba4b307be84428c98b0a12f334285600080878760405161012a9796959493929190610475565b60405180910390a1505050565b600061014a610145846105bb565b610596565b9050808382526020820190508285602086028201111561016957600080fd5b60005b85811015610199578161017f888261020b565b84526020840193506020830192505060018101905061016c565b5050509392505050565b60006101b66101b1846105e7565b610596565b9050828152602081018484840111156101ce57600080fd5b6101d98482856106e4565b509392505050565b600082601f8301126101f257600080fd5b8135610202848260208601610137565b91505092915050565b60008135905061021a816107a7565b92915050565b600082601f83011261023157600080fd5b81356102418482602086016101a3565b91505092915050565b6000806000806080858703121561026057600080fd5b600061026e8782880161020b565b945050602061027f8782880161020b565b935050604085013567ffffffffffffffff81111561029c57600080fd5b6102a887828801610220565b925050606085013567ffffffffffffffff8111156102c557600080fd5b6102d1878288016101e1565b91505092959194509250565b600080604083850312156102f057600080fd5b600083013567ffffffffffffffff81111561030a57600080fd5b61031685828601610220565b925050602083013567ffffffffffffffff81111561033357600080fd5b61033f85828601610220565b9150509250929050565b600061035583836103ce565b60208301905092915050565b61036a8161066d565b82525050565b600061037b82610628565b610385818561064b565b935061039083610618565b8060005b838110156103c15781516103a88882610349565b97506103b38361063e565b925050600181019050610394565b5085935050505092915050565b6103d78161067f565b82525050565b6103e68161067f565b82525050565b6103f5816106ca565b82525050565b600061040682610633565b610410818561065c565b93506104208185602086016106f3565b61042981610786565b840191505092915050565b600061044160008361065c565b915061044c826107a4565b600082019050919050565b610460816106b3565b82525050565b61046f816106bd565b82525050565b600060e08201905061048a600083018a610361565b6104976020830189610457565b81810360408301526104a981886103fb565b90506104b860608301876103ec565b6104c560808301866103ec565b81810360a08301526104d781856103fb565b905081810360c08301526104eb8184610370565b905098975050505050505050565b600060e08201905061050e6000830189610361565b61051b6020830188610457565b818103604083015261052c81610434565b905061053b60608301876103dd565b61054860808301866103dd565b81810360a083015261055a81856103fb565b905081810360c083015261056e8184610370565b9050979650505050505050565b60006020820190506105906000830184610466565b92915050565b60006105a06105b1565b90506105ac8282610726565b919050565b6000604051905090565b600067ffffffffffffffff8211156105d6576105d5610757565b5b602082029050602081019050919050565b600067ffffffffffffffff82111561060257610601610757565b5b61060b82610786565b9050602081019050919050565b6000819050602082019050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b600082825260208201905092915050565b600082825260208201905092915050565b600061067882610693565b9050919050565b6000819050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600060ff82169050919050565b60006106dd6106d883610689565b610797565b9050919050565b82818337600083830152505050565b60005b838110156107115780820151818401526020810190506106f6565b83811115610720576000848401525b50505050565b61072f82610786565b810181811067ffffffffffffffff8211171561074e5761074d610757565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b60008160001b9050919050565b50565b6107b08161067f565b81146107bb57600080fd5b5056fea2646970667358221220a44450aaa7ba64c5bc6267debd3b45265c23dee5b8905a7ee88ca3d86721958a64736f6c63430008040033",
+  "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100415760003560e01c806348ce1dcc1461004657806397eacfe614610062578063fb9066c114610080575b600080fd5b610060600480360381019061005b919061024a565b61009c565b005b61006a6100e3565b604051610077919061057b565b60405180910390f35b61009a600480360381019061009591906102dd565b6100ec565b005b7f805721bc246bccc732581be0c0aa2dd8f7ec93e97ba4b307be84428c98b0a12f3342868686866040516100d5969594939291906104f9565b60405180910390a150505050565b60006002905090565b60607f805721bc246bccc732581be0c0aa2dd8f7ec93e97ba4b307be84428c98b0a12f334285600080878760405161012a9796959493929190610475565b60405180910390a1505050565b600061014a610145846105bb565b610596565b9050808382526020820190508285602086028201111561016957600080fd5b60005b85811015610199578161017f888261020b565b84526020840193506020830192505060018101905061016c565b5050509392505050565b60006101b66101b1846105e7565b610596565b9050828152602081018484840111156101ce57600080fd5b6101d98482856106e4565b509392505050565b600082601f8301126101f257600080fd5b8135610202848260208601610137565b91505092915050565b60008135905061021a816107a7565b92915050565b600082601f83011261023157600080fd5b81356102418482602086016101a3565b91505092915050565b6000806000806080858703121561026057600080fd5b600061026e8782880161020b565b945050602061027f8782880161020b565b935050604085013567ffffffffffffffff81111561029c57600080fd5b6102a887828801610220565b925050606085013567ffffffffffffffff8111156102c557600080fd5b6102d1878288016101e1565b91505092959194509250565b600080604083850312156102f057600080fd5b600083013567ffffffffffffffff81111561030a57600080fd5b61031685828601610220565b925050602083013567ffffffffffffffff81111561033357600080fd5b61033f85828601610220565b9150509250929050565b600061035583836103ce565b60208301905092915050565b61036a8161066d565b82525050565b600061037b82610628565b610385818561064b565b935061039083610618565b8060005b838110156103c15781516103a88882610349565b97506103b38361063e565b925050600181019050610394565b5085935050505092915050565b6103d78161067f565b82525050565b6103e68161067f565b82525050565b6103f5816106ca565b82525050565b600061040682610633565b610410818561065c565b93506104208185602086016106f3565b61042981610786565b840191505092915050565b600061044160008361065c565b915061044c826107a4565b600082019050919050565b610460816106b3565b82525050565b61046f816106bd565b82525050565b600060e08201905061048a600083018a610361565b6104976020830189610457565b81810360408301526104a981886103fb565b90506104b860608301876103ec565b6104c560808301866103ec565b81810360a08301526104d781856103fb565b905081810360c08301526104eb8184610370565b905098975050505050505050565b600060e08201905061050e6000830189610361565b61051b6020830188610457565b818103604083015261052c81610434565b905061053b60608301876103dd565b61054860808301866103dd565b81810360a083015261055a81856103fb565b905081810360c083015261056e8184610370565b9050979650505050505050565b60006020820190506105906000830184610466565b92915050565b60006105a06105b1565b90506105ac8282610726565b919050565b6000604051905090565b600067ffffffffffffffff8211156105d6576105d5610757565b5b602082029050602081019050919050565b600067ffffffffffffffff82111561060257610601610757565b5b61060b82610786565b9050602081019050919050565b6000819050602082019050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b600082825260208201905092915050565b600082825260208201905092915050565b600061067882610693565b9050919050565b6000819050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600060ff82169050919050565b60006106dd6106d883610689565b610797565b9050919050565b82818337600083830152505050565b60005b838110156107115780820151818401526020810190506106f6565b83811115610720576000848401525b50505050565b61072f82610786565b810181811067ffffffffffffffff8211171561074e5761074d610757565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b60008160001b9050919050565b50565b6107b08161067f565b81146107bb57600080fd5b5056fea2646970667358221220a44450aaa7ba64c5bc6267debd3b45265c23dee5b8905a7ee88ca3d86721958a64736f6c63430008040033",
+  "linkReferences": {},
+  "deployedLinkReferences": {}
+}
diff --git a/test/data/contracts/firefly/FireflyV1.json b/test/data/contracts/firefly/FireflyV1.json
new file mode 100644
index 000000000..5329460d5
--- /dev/null
+++ b/test/data/contracts/firefly/FireflyV1.json
@@ -0,0 +1,93 @@
+{
+  "_format": "hh-sol-artifact-1",
+  "contractName": "Firefly",
+  "sourceName": "contracts/Firefly.sol",
+  "abi": [
+    {
+      "anonymous": false,
+      "inputs": [
+        {
+          "indexed": false,
+          "internalType": "address",
+          "name": "author",
+          "type": "address"
+        },
+        {
+          "indexed": false,
+          "internalType": "uint256",
+          "name": "timestamp",
+          "type": "uint256"
+        },
+        {
+          "indexed": false,
+          "internalType": "string",
+          "name": "namespace",
+          "type": "string"
+        },
+        {
+          "indexed": false,
+          "internalType": "bytes32",
+          "name": "uuids",
+          "type": "bytes32"
+        },
+        {
+          "indexed": false,
+          "internalType": "bytes32",
+          "name": "batchHash",
+          "type": "bytes32"
+        },
+        {
+          "indexed": false,
+          "internalType": "string",
+          "name": "payloadRef",
+          "type": "string"
+        },
+        {
+          "indexed": false,
+          "internalType": "bytes32[]",
+          "name": "contexts",
+          "type": "bytes32[]"
+        }
+      ],
+      "name": "BatchPin",
+      "type": "event"
+    },
+    {
+      "inputs": [
+        {
+          "internalType": "string",
+          "name": "namespace",
+          "type": "string"
+        },
+        {
+          "internalType": "bytes32",
+          "name": "uuids",
+          "type": "bytes32"
+        },
+        {
+          "internalType": "bytes32",
+          "name": "batchHash",
+          "type": "bytes32"
+        },
+        {
+          "internalType": "string",
+          "name": "payloadRef",
+          "type": "string"
+        },
+        {
+          "internalType": "bytes32[]",
+          "name": "contexts",
+          "type": "bytes32[]"
+        }
+      ],
+      "name": "pinBatch",
+      "outputs": [],
+      "stateMutability": "nonpayable",
+      "type": "function"
+    }
+  ],
+  "bytecode": "0x608060405234801561001057600080fd5b506105f4806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80633674e15c14610030575b600080fd5b61004a600480360381019061004591906101a9565b61004c565b005b7f805721bc246bccc732581be0c0aa2dd8f7ec93e97ba4b307be84428c98b0a12f334287878787876040516100879796959493929190610353565b60405180910390a15050505050565b60006100a96100a4846103fc565b6103d7565b905080838252602082019050828560208602820111156100c857600080fd5b60005b858110156100f857816100de888261016a565b8452602084019350602083019250506001810190506100cb565b5050509392505050565b600061011561011084610428565b6103d7565b90508281526020810184848401111561012d57600080fd5b6101388482856104f4565b509392505050565b600082601f83011261015157600080fd5b8135610161848260208601610096565b91505092915050565b600081359050610179816105a7565b92915050565b600082601f83011261019057600080fd5b81356101a0848260208601610102565b91505092915050565b600080600080600060a086880312156101c157600080fd5b600086013567ffffffffffffffff8111156101db57600080fd5b6101e78882890161017f565b95505060206101f88882890161016a565b94505060406102098882890161016a565b935050606086013567ffffffffffffffff81111561022657600080fd5b6102328882890161017f565b925050608086013567ffffffffffffffff81111561024f57600080fd5b61025b88828901610140565b9150509295509295909350565b600061027483836102ed565b60208301905092915050565b610289816104ae565b82525050565b600061029a82610469565b6102a4818561048c565b93506102af83610459565b8060005b838110156102e05781516102c78882610268565b97506102d28361047f565b9250506001810190506102b3565b5085935050505092915050565b6102f6816104c0565b82525050565b610305816104c0565b82525050565b600061031682610474565b610320818561049d565b9350610330818560208601610503565b61033981610596565b840191505092915050565b61034d816104ea565b82525050565b600060e082019050610368600083018a610280565b6103756020830189610344565b8181036040830152610387818861030b565b905061039660608301876102fc565b6103a360808301866102fc565b81810360a08301526103b5818561030b565b905081810360c08301526103c9818461028f565b905098975050505050505050565b60006103e16103f2565b90506103ed8282610536565b919050565b6000604051905090565b600067ffffffffffffffff82111561041757610416610567565b5b602082029050602081019050919050565b600067ffffffffffffffff82111561044357610442610567565b5b61044c82610596565b9050602081019050919050565b6000819050602082019050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b600082825260208201905092915050565b600082825260208201905092915050565b60006104b9826104ca565b9050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b82818337600083830152505050565b60005b83811015610521578082015181840152602081019050610506565b83811115610530576000848401525b50505050565b61053f82610596565b810181811067ffffffffffffffff8211171561055e5761055d610567565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b6105b0816104c0565b81146105bb57600080fd5b5056fea264697066735822122061023cd0c04051ec5c4b0d519dff2ca240ba0dee202ae2aceb72c566f049faf764736f6c63430008040033",
+  "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061002b5760003560e01c80633674e15c14610030575b600080fd5b61004a600480360381019061004591906101a9565b61004c565b005b7f805721bc246bccc732581be0c0aa2dd8f7ec93e97ba4b307be84428c98b0a12f334287878787876040516100879796959493929190610353565b60405180910390a15050505050565b60006100a96100a4846103fc565b6103d7565b905080838252602082019050828560208602820111156100c857600080fd5b60005b858110156100f857816100de888261016a565b8452602084019350602083019250506001810190506100cb565b5050509392505050565b600061011561011084610428565b6103d7565b90508281526020810184848401111561012d57600080fd5b6101388482856104f4565b509392505050565b600082601f83011261015157600080fd5b8135610161848260208601610096565b91505092915050565b600081359050610179816105a7565b92915050565b600082601f83011261019057600080fd5b81356101a0848260208601610102565b91505092915050565b600080600080600060a086880312156101c157600080fd5b600086013567ffffffffffffffff8111156101db57600080fd5b6101e78882890161017f565b95505060206101f88882890161016a565b94505060406102098882890161016a565b935050606086013567ffffffffffffffff81111561022657600080fd5b6102328882890161017f565b925050608086013567ffffffffffffffff81111561024f57600080fd5b61025b88828901610140565b9150509295509295909350565b600061027483836102ed565b60208301905092915050565b610289816104ae565b82525050565b600061029a82610469565b6102a4818561048c565b93506102af83610459565b8060005b838110156102e05781516102c78882610268565b97506102d28361047f565b9250506001810190506102b3565b5085935050505092915050565b6102f6816104c0565b82525050565b610305816104c0565b82525050565b600061031682610474565b610320818561049d565b9350610330818560208601610503565b61033981610596565b840191505092915050565b61034d816104ea565b82525050565b600060e082019050610368600083018a610280565b6103756020830189610344565b8181036040830152610387818861030b565b905061039660608301876102fc565b6103a360808301866102fc565b81810360a08301526103b5818561030b565b905081810360c08301526103c9818461028f565b905098975050505050505050565b60006103e16103f2565b90506103ed8282610536565b919050565b6000604051905090565b600067ffffffffffffffff82111561041757610416610567565b5b602082029050602081019050919050565b600067ffffffffffffffff82111561044357610442610567565b5b61044c82610596565b9050602081019050919050565b6000819050602082019050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b600082825260208201905092915050565b600082825260208201905092915050565b60006104b9826104ca565b9050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b82818337600083830152505050565b60005b83811015610521578082015181840152602081019050610506565b83811115610530576000848401525b50505050565b61053f82610596565b810181811067ffffffffffffffff8211171561055e5761055d610567565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b6105b0816104c0565b81146105bb57600080fd5b5056fea264697066735822122061023cd0c04051ec5c4b0d519dff2ca240ba0dee202ae2aceb72c566f049faf764736f6c63430008040033",
+  "linkReferences": {},
+  "deployedLinkReferences": {}
+}
diff --git a/test/e2e/client/restclient.go b/test/e2e/client/restclient.go
index 9fca08af8..bdc938fe5 100644
--- a/test/e2e/client/restclient.go
+++ b/test/e2e/client/restclient.go
@@ -68,6 +68,9 @@ var (
 	urlBlockchainEvents  = "/blockchainevents"
 	urlOperations        = "/operations"
 	urlGetOrganizations  = "/network/organizations"
+	urlOrganizationsSelf = "/network/organizations/self"
+	urlNodesSelf         = "/network/nodes/self"
+	urlNetworkAction     = "/network/action"
 	urlGetOrgKeys        = "/identities/%s/verifiers"
 )
 
@@ -206,17 +209,39 @@ func (client *FireFlyClient) GetBlob(t *testing.T, data *core.Data, expectedStat
 	return blob
 }
 
-func (client *FireFlyClient) GetOrgs(t *testing.T, expectedStatus int) (orgs []*core.Identity) {
+func (client *FireFlyClient) GetOrgs(t *testing.T) (orgs []*core.Identity) {
 	path := client.namespaced(urlGetOrganizations)
 	resp, err := client.Client.R().
 		SetQueryParam("sort", "created").
 		SetResult(&orgs).
 		Get(path)
 	require.NoError(t, err)
-	require.Equal(t, expectedStatus, resp.StatusCode(), "GET %s [%d]: %s", path, resp.StatusCode(), resp.String())
+	require.Equal(t, 200, resp.StatusCode(), "GET %s [%d]: %s", path, resp.StatusCode(), resp.String())
 	return orgs
 }
 
+func (client *FireFlyClient) RegisterSelfOrg(t *testing.T, confirm bool) {
+	path := client.namespaced(urlOrganizationsSelf)
+	input := make(map[string]interface{})
+	resp, err := client.Client.R().
+		SetQueryParam("confirm", strconv.FormatBool(confirm)).
+		SetBody(input).
+		Post(path)
+	require.NoError(t, err)
+	require.Equal(t, 200, resp.StatusCode(), "POST %s [%d]: %s", path, resp.StatusCode(), resp.String())
+}
+
+func (client *FireFlyClient) RegisterSelfNode(t *testing.T, confirm bool) {
+	path := client.namespaced(urlNodesSelf)
+	input := make(map[string]interface{})
+	resp, err := client.Client.R().
+		SetQueryParam("confirm", strconv.FormatBool(confirm)).
+		SetBody(input).
+		Post(path)
+	require.NoError(t, err)
+	require.Equal(t, 200, resp.StatusCode(), "POST %s [%d]: %s", path, resp.StatusCode(), resp.String())
+}
+
 func (client *FireFlyClient) GetIdentityBlockchainKeys(t *testing.T, identityID *fftypes.UUID, expectedStatus int) (verifiers []*core.Verifier) {
 	path := client.namespaced(fmt.Sprintf(urlGetOrgKeys, identityID))
 	resp, err := client.Client.R().
@@ -284,7 +309,7 @@ func (client *FireFlyClient) BroadcastMessageAsIdentity(t *testing.T, did, topic
 		SetQueryParam("confirm", strconv.FormatBool(confirm)).
 		SetResult(&msg).
 		Post(client.namespaced(urlBroadcastMessage))
-	t.Logf("Sent broadcast msg: %s", msg.Header.ID)
+	client.logger.Logf("Sent broadcast msg: %s", msg.Header.ID)
 	return res, err
 }
 
@@ -306,7 +331,7 @@ func (client *FireFlyClient) ClaimCustomIdentity(t *testing.T, key, name, desc s
 		Post(client.namespaced(urlIdentities))
 	assert.NoError(t, err)
 	assert.True(t, res.IsSuccess())
-	t.Logf("Identity creation initiated with key %s: %s (%s)", key, identity.ID, identity.DID)
+	client.logger.Logf("Identity creation initiated with key %s: %s (%s)", key, identity.ID, identity.DID)
 	return &identity
 }
 
@@ -320,6 +345,19 @@ func (client *FireFlyClient) GetIdentity(t *testing.T, id *fftypes.UUID) *core.I
 	return &identity
 }
 
+func (client *FireFlyClient) GetOrganization(t *testing.T, idOrName string) *core.Identity {
+	var identity core.Identity
+	res, err := client.Client.R().
+		SetResult(&identity).
+		Get(client.namespaced(fmt.Sprintf("%s/%s", urlGetOrganizations, idOrName)))
+	assert.NoError(t, err)
+	if res.StatusCode() == 404 {
+		return nil
+	}
+	assert.True(t, res.IsSuccess())
+	return &identity
+}
+
 func (client *FireFlyClient) GetVerifiers(t *testing.T) []*core.Verifier {
 	var verifiers []*core.Verifier
 	res, err := client.Client.R().
@@ -337,7 +375,7 @@ func (client *FireFlyClient) CreateBlob(t *testing.T, dt *core.DatatypeRef) *cor
 		blob[i] = byte('a' + i%26)
 	}
 	var blobHash fftypes.Bytes32 = sha256.Sum256(blob)
-	t.Logf("Blob size=%d hash=%s", len(blob), &blobHash)
+	client.logger.Logf("Blob size=%d hash=%s", len(blob), &blobHash)
 	var data core.Data
 	formData := map[string]string{}
 	if dt == nil {
@@ -359,7 +397,7 @@ func (client *FireFlyClient) CreateBlob(t *testing.T, dt *core.DatatypeRef) *cor
 		Post(path)
 	require.NoError(t, err)
 	require.Equal(t, 201, resp.StatusCode(), "POST %s [%d]: %s", path, resp.StatusCode(), resp.String())
-	t.Logf("Data created: %s", data.ID)
+	client.logger.Logf("Data created: %s", data.ID)
 	if dt == nil {
 		assert.Equal(t, "data", data.Value.JSONObject().GetString("mymeta"))
 		assert.Equal(t, "myfile.txt", data.Value.JSONObject().GetString("filename"))
@@ -833,26 +871,37 @@ func (client *FireFlyClient) CreateContractAPIListener(t *testing.T, apiName, ev
 	return &listener, err
 }
 
-func (client *FireFlyClient) GetEvent(t *testing.T, eventID string) (interface{}, error) {
-	var res interface{}
+func (client *FireFlyClient) GetEvent(t *testing.T, eventID string) *core.Event {
+	var ev core.Event
 	path := client.namespaced(fmt.Sprintf("%s/%s", urlGetEvents, eventID))
 	resp, err := client.Client.R().
-		SetResult(&res).
+		SetResult(&ev).
 		Get(path)
 	require.NoError(t, err)
 	require.Equal(t, 200, resp.StatusCode(), "GET %s [%d]: %s", path, resp.StatusCode(), resp.String())
-	return res, err
+	return &ev
 }
 
-func (client *FireFlyClient) GetBlockchainEvent(t *testing.T, eventID string) (interface{}, error) {
-	var res interface{}
+func (client *FireFlyClient) GetBlockchainEvents(t *testing.T, startTime time.Time) (events []*core.BlockchainEvent) {
+	path := client.namespaced(urlBlockchainEvents)
+	resp, err := client.Client.R().
+		SetQueryParam("timestamp", fmt.Sprintf(">%d", startTime.UnixNano())).
+		SetResult(&events).
+		Get(path)
+	require.NoError(t, err)
+	require.Equal(t, 200, resp.StatusCode(), "GET %s [%d]: %s", path, resp.StatusCode(), resp.String())
+	return events
+}
+
+func (client *FireFlyClient) GetBlockchainEvent(t *testing.T, eventID string) *core.BlockchainEvent {
+	var ev core.BlockchainEvent
 	path := client.namespaced(fmt.Sprintf("%s/%s", urlBlockchainEvents, eventID))
 	resp, err := client.Client.R().
-		SetResult(&res).
+		SetResult(&ev).
 		Get(path)
 	require.NoError(t, err)
 	require.Equal(t, 200, resp.StatusCode(), "GET %s [%d]: %s", path, resp.StatusCode(), resp.String())
-	return res, err
+	return &ev
 }
 
 func (client *FireFlyClient) GetOperations(t *testing.T, startTime time.Time) (operations []*core.Operation) {
@@ -865,3 +914,13 @@ func (client *FireFlyClient) GetOperations(t *testing.T, startTime time.Time) (o
 	require.Equal(t, 200, resp.StatusCode(), "GET %s [%d]: %s", path, resp.StatusCode(), resp.String())
 	return operations
 }
+
+func (client *FireFlyClient) NetworkAction(t *testing.T, action core.NetworkActionType) {
+	path := client.namespaced(urlNetworkAction)
+	input := &core.NetworkAction{Type: action}
+	resp, err := client.Client.R().
+		SetBody(input).
+		Post(path)
+	require.NoError(t, err)
+	require.Equal(t, 202, resp.StatusCode(), "POST %s [%d]: %s", path, resp.StatusCode(), resp.String())
+}
diff --git a/test/e2e/e2e.go b/test/e2e/e2e.go
index 759e4bc09..29cdcd969 100644
--- a/test/e2e/e2e.go
+++ b/test/e2e/e2e.go
@@ -103,7 +103,7 @@ func ReadStackState(t *testing.T) *StackState {
 	return stackState
 }
 
-func WsReader(conn *websocket.Conn, dbChanges bool) chan *core.EventDelivery {
+func WsReader(conn *websocket.Conn) chan *core.EventDelivery {
 	events := make(chan *core.EventDelivery, 100)
 	go func() {
 		for {
@@ -168,18 +168,14 @@ func WaitForContractEvent(t *testing.T, client *client.FireFlyClient, c chan *co
 	for {
 		eventDelivery := <-c
 		if eventDelivery.Type == core.EventTypeBlockchainEventReceived {
-			event, err := client.GetBlockchainEvent(t, eventDelivery.Event.Reference.String())
-			if err != nil {
-				t.Logf("WARN: unable to get event: %v", err.Error())
-				continue
-			}
-			eventJSON, ok := event.(map[string]interface{})
-			if !ok {
-				t.Logf("WARN: unable to parse changeEvent: %v", event)
-				continue
-			}
-			if checkObject(t, match, eventJSON) {
-				return eventJSON
+			event := client.GetBlockchainEvent(t, eventDelivery.Event.Reference.String())
+			eventJSON, err := json.Marshal(&event)
+			require.NoError(t, err)
+			var eventMap map[string]interface{}
+			err = json.Unmarshal(eventJSON, &eventMap)
+			require.NoError(t, err)
+			if checkObject(t, match, eventMap) {
+				return eventMap
 			}
 		}
 	}
diff --git a/test/e2e/gateway/gateway_test.go b/test/e2e/gateway/common.go
similarity index 90%
rename from test/e2e/gateway/gateway_test.go
rename to test/e2e/gateway/common.go
index 764a5afa5..8f1fef2b9 100644
--- a/test/e2e/gateway/gateway_test.go
+++ b/test/e2e/gateway/common.go
@@ -17,6 +17,7 @@
 package gateway
 
 import (
+	"crypto/rand"
 	"encoding/base64"
 	"fmt"
 	"net/http"
@@ -27,6 +28,12 @@ import (
 	"github.com/gorilla/websocket"
 	"github.com/hyperledger/firefly/test/e2e"
 	"github.com/hyperledger/firefly/test/e2e/client"
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	schemeHTTP  = "http"
+	schemeHTTPS = "https"
 )
 
 type testState struct {
@@ -57,9 +64,9 @@ func beforeE2ETest(t *testing.T) *testState {
 
 	var authHeader1 http.Header
 
-	httpProtocolClient1 := "http"
+	httpProtocolClient1 := schemeHTTP
 	if stack.Members[0].UseHTTPS {
-		httpProtocolClient1 = "https"
+		httpProtocolClient1 = schemeHTTPS
 	}
 
 	member0WithPort := ""
@@ -110,3 +117,10 @@ func beforeE2ETest(t *testing.T) *testState {
 	}
 	return ts
 }
+
+func randomName(t *testing.T) string {
+	b := make([]byte, 5)
+	_, err := rand.Read(b)
+	assert.NoError(t, err)
+	return fmt.Sprintf("e2e_%x", b)
+}
diff --git a/test/e2e/gateway/ethereum_contract_test.go b/test/e2e/gateway/ethereum_contracts.go
similarity index 95%
rename from test/e2e/gateway/ethereum_contract_test.go
rename to test/e2e/gateway/ethereum_contracts.go
index 06a6d247d..06c9d5abc 100644
--- a/test/e2e/gateway/ethereum_contract_test.go
+++ b/test/e2e/gateway/ethereum_contracts.go
@@ -35,22 +35,6 @@ import (
 
 var contractVersion, _ = nanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", nanoid.DefaultSize)
 
-type uploadABIResult struct {
-	ID string `json:"id"`
-}
-
-type deployABIResult struct {
-	ContractAddress string `json:"contractAddress"`
-}
-
-type ethconnectOutput struct {
-	Output string `json:"output"`
-}
-
-type simpleStorageBody struct {
-	NewValue string `json:"newValue"`
-}
-
 func simpleStorageFFIChanged() *fftypes.FFIEvent {
 	return &fftypes.FFIEvent{
 		FFIEventDefinition: fftypes.FFIEventDefinition{
@@ -110,7 +94,8 @@ func simpleStorageFFIGet() *fftypes.FFIMethod {
 }
 
 func deployContract(t *testing.T, stackName, contract string) string {
-	out, err := exec.Command("ff", "deploy", "ethereum", stackName, "../../data/contracts/"+contract).Output()
+	path := "../../data/contracts/" + contract
+	out, err := exec.Command("ff", "deploy", "ethereum", stackName, path).Output()
 	require.NoError(t, err)
 	var output map[string]interface{}
 	err = json.Unmarshal(out, &output)
@@ -156,7 +141,7 @@ func (suite *EthereumContractTestSuite) AfterTest(suiteName, testName string) {
 func (suite *EthereumContractTestSuite) TestDirectInvokeMethod() {
 	defer suite.testState.Done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, true)
+	received1 := e2e.WsReader(suite.testState.ws1)
 	listener := suite.testState.client1.CreateContractListener(suite.T(), simpleStorageFFIChanged(), &fftypes.JSONObject{
 		"address": suite.contractAddress,
 	})
@@ -210,7 +195,7 @@ func (suite *EthereumContractTestSuite) TestDirectInvokeMethod() {
 func (suite *EthereumContractTestSuite) TestFFIInvokeMethod() {
 	defer suite.testState.Done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, true)
+	received1 := e2e.WsReader(suite.testState.ws1)
 
 	ffiReference := &fftypes.FFIReference{
 		ID: suite.interfaceID,
@@ -269,7 +254,7 @@ func (suite *EthereumContractTestSuite) TestFFIInvokeMethod() {
 func (suite *EthereumContractTestSuite) TestContractAPIMethod() {
 	defer suite.testState.Done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, true)
+	received1 := e2e.WsReader(suite.testState.ws1)
 	APIName := fftypes.NewUUID().String()
 
 	ffiReference := &fftypes.FFIReference{
diff --git a/test/e2e/gateway/fabric_contract_test.go b/test/e2e/gateway/fabric_contracts.go
similarity index 84%
rename from test/e2e/gateway/fabric_contract_test.go
rename to test/e2e/gateway/fabric_contracts.go
index 6aac55739..7a7411b4b 100644
--- a/test/e2e/gateway/fabric_contract_test.go
+++ b/test/e2e/gateway/fabric_contracts.go
@@ -34,19 +34,6 @@ import (
 	"github.com/stretchr/testify/suite"
 )
 
-type fabConnectHeaders struct {
-	Type      string `json:"type"`
-	Channel   string `json:"channel"`
-	Signer    string `json:"signer"`
-	Chaincode string `json:"chaincode"`
-}
-
-type createAssetBody struct {
-	Headers fabConnectHeaders `json:"headers"`
-	Func    string            `json:"func"`
-	Args    []string          `json:"args"`
-}
-
 var assetCreatedEvent = &fftypes.FFIEvent{
 	FFIEventDefinition: fftypes.FFIEventDefinition{
 		Name: "AssetCreated",
@@ -101,26 +88,6 @@ func deployChaincode(t *testing.T, stackName string) string {
 	return chaincodeName
 }
 
-func invokeFabContract(t *testing.T, client *resty.Client, channel, chaincode, signer, method string, args []string) {
-	path := "/transactions"
-	body := &createAssetBody{
-		Headers: fabConnectHeaders{
-			Type:      "SendTransaction",
-			Channel:   channel,
-			Signer:    signer,
-			Chaincode: chaincode,
-		},
-		Func: method,
-		Args: args,
-	}
-	resp, err := client.R().
-		SetHeader("x-firefly-sync", "true").
-		SetBody(body).
-		Post(path)
-	require.NoError(t, err)
-	require.Equal(t, 200, resp.StatusCode(), "POST %s [%d]: %s", path, resp.StatusCode(), resp.String())
-}
-
 type FabricContractTestSuite struct {
 	suite.Suite
 	testState     *testState
@@ -147,7 +114,7 @@ func (suite *FabricContractTestSuite) AfterTest(suiteName, testName string) {
 func (suite *FabricContractTestSuite) TestE2EContractEvents() {
 	defer suite.testState.done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, true)
+	received1 := e2e.WsReader(suite.testState.ws1)
 
 	sub := suite.testState.client1.CreateContractListener(suite.T(), assetCreatedEvent, &fftypes.JSONObject{
 		"channel":   "firefly",
diff --git a/test/e2e/gateway/tokens_test.go b/test/e2e/gateway/tokens.go
similarity index 91%
rename from test/e2e/gateway/tokens_test.go
rename to test/e2e/gateway/tokens.go
index 196d49de9..81cb49a22 100644
--- a/test/e2e/gateway/tokens_test.go
+++ b/test/e2e/gateway/tokens.go
@@ -18,8 +18,6 @@ package gateway
 
 import (
 	"fmt"
-	"math/rand"
-	"time"
 
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/pkg/core"
@@ -56,11 +54,9 @@ func (suite *TokensTestSuite) AfterTest(suiteName, testName string) {
 func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 	defer suite.testState.done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, false)
+	received1 := e2e.WsReader(suite.testState.ws1)
 
-	pools := suite.testState.client1.GetTokenPools(suite.T(), time.Unix(0, 0))
-	rand.Seed(time.Now().UnixNano())
-	poolName := fmt.Sprintf("pool%d", rand.Intn(10000))
+	poolName := fmt.Sprintf("pool_%s", randomName(suite.T()))
 	suite.T().Logf("Pool name: %s", poolName)
 
 	pool := &core.TokenPool{
@@ -73,7 +69,7 @@ func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 	poolID := poolResp.ID
 
 	e2e.WaitForEvent(suite.T(), received1, core.EventTypePoolConfirmed, poolID)
-	pools = suite.testState.client1.GetTokenPools(suite.T(), suite.testState.startTime)
+	pools := suite.testState.client1.GetTokenPools(suite.T(), suite.testState.startTime)
 	assert.Equal(suite.T(), 1, len(pools))
 	assert.Equal(suite.T(), suite.testState.namespace, pools[0].Namespace)
 	assert.Equal(suite.T(), suite.connector, pools[0].Connector)
diff --git a/test/e2e/multiparty/multiparty_test.go b/test/e2e/multiparty/common.go
similarity index 94%
rename from test/e2e/multiparty/multiparty_test.go
rename to test/e2e/multiparty/common.go
index 1da5aa624..b13c4f189 100644
--- a/test/e2e/multiparty/multiparty_test.go
+++ b/test/e2e/multiparty/common.go
@@ -18,6 +18,7 @@ package multiparty
 
 import (
 	"context"
+	"crypto/rand"
 	"crypto/sha256"
 	"encoding/base64"
 	"fmt"
@@ -34,6 +35,11 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+const (
+	schemeHTTP  = "http"
+	schemeHTTPS = "https"
+)
+
 type testState struct {
 	t                    *testing.T
 	startTime            time.Time
@@ -69,13 +75,13 @@ func beforeE2ETest(t *testing.T) *testState {
 	var authHeader1 http.Header
 	var authHeader2 http.Header
 
-	httpProtocolClient1 := "http"
-	httpProtocolClient2 := "http"
+	httpProtocolClient1 := schemeHTTP
+	httpProtocolClient2 := schemeHTTP
 	if stack.Members[0].UseHTTPS {
-		httpProtocolClient1 = "https"
+		httpProtocolClient1 = schemeHTTPS
 	}
 	if stack.Members[1].UseHTTPS {
-		httpProtocolClient2 = "https"
+		httpProtocolClient2 = schemeHTTPS
 	}
 
 	member0WithPort := ""
@@ -133,8 +139,8 @@ func beforeE2ETest(t *testing.T) *testState {
 	e2e.PollForUp(t, ts.client2)
 
 	for {
-		orgsC1 := ts.client1.GetOrgs(t, 200)
-		orgsC2 := ts.client2.GetOrgs(t, 200)
+		orgsC1 := ts.client1.GetOrgs(t)
+		orgsC2 := ts.client2.GetOrgs(t)
 		if len(orgsC1) >= 2 && len(orgsC2) >= 2 {
 			// in case there are more than two orgs in the network we need to ensure
 			// we select the same two that were provided in the first two elements
@@ -228,3 +234,10 @@ func validateReceivedMessages(ts *testState, client *client.FireFlyClient, topic
 	// Flip data (returned in most recent order) into delivery order
 	return returnData
 }
+
+func randomName(t *testing.T) string {
+	b := make([]byte, 5)
+	_, err := rand.Read(b)
+	assert.NoError(t, err)
+	return fmt.Sprintf("e2e_%x", b)
+}
diff --git a/test/e2e/multiparty/contract_migration.go b/test/e2e/multiparty/contract_migration.go
new file mode 100644
index 000000000..2f7262fee
--- /dev/null
+++ b/test/e2e/multiparty/contract_migration.go
@@ -0,0 +1,227 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package multiparty
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/go-resty/resty/v2"
+	"github.com/hyperledger/firefly-common/pkg/fftypes"
+	"github.com/hyperledger/firefly/pkg/core"
+	"github.com/hyperledger/firefly/test/e2e"
+	"github.com/hyperledger/firefly/test/e2e/client"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+	"gopkg.in/yaml.v2"
+)
+
+func readConfig(t *testing.T, configFile string) map[string]interface{} {
+	yfile, err := ioutil.ReadFile(configFile)
+	assert.NoError(t, err)
+	data := make(map[string]interface{})
+	err = yaml.Unmarshal(yfile, &data)
+	assert.NoError(t, err)
+	return data
+}
+
+func writeConfig(t *testing.T, configFile string, data map[string]interface{}) {
+	out, err := yaml.Marshal(data)
+	assert.NoError(t, err)
+	f, err := os.Create(configFile)
+	assert.NoError(t, err)
+	_, err = f.Write(out)
+	assert.NoError(t, err)
+	f.Close()
+}
+
+func addNamespace(data map[string]interface{}, ns map[string]interface{}) {
+	namespaces := data["namespaces"].(map[interface{}]interface{})
+	predefined := namespaces["predefined"].([]interface{})
+	namespaces["predefined"] = append(predefined, ns)
+}
+
+func resetFireFly(t *testing.T, client *resty.Client) {
+	resp, err := client.R().
+		SetBody(map[string]interface{}{}).
+		Post("/reset")
+	require.NoError(t, err)
+	assert.Equal(t, 204, resp.StatusCode())
+}
+
+type ContractMigrationTestSuite struct {
+	suite.Suite
+	testState   *testState
+	stackName   string
+	adminHost1  string
+	adminHost2  string
+	configFile1 string
+	configFile2 string
+}
+
+func (suite *ContractMigrationTestSuite) SetupSuite() {
+	suite.testState = beforeE2ETest(suite.T())
+	stack := e2e.ReadStack(suite.T())
+	suite.stackName = stack.Name
+
+	adminProtocol1 := schemeHTTP
+	if stack.Members[0].UseHTTPS {
+		adminProtocol1 = schemeHTTPS
+	}
+	adminProtocol2 := schemeHTTP
+	if stack.Members[1].UseHTTPS {
+		adminProtocol2 = schemeHTTPS
+	}
+	suite.adminHost1 = fmt.Sprintf("%s://%s:%d", adminProtocol1, stack.Members[0].FireflyHostname, stack.Members[0].ExposedAdminPort)
+	suite.adminHost2 = fmt.Sprintf("%s://%s:%d", adminProtocol2, stack.Members[1].FireflyHostname, stack.Members[1].ExposedAdminPort)
+
+	stackDir := os.Getenv("STACK_DIR")
+	if stackDir == "" {
+		suite.T().Fatal("STACK_DIR must be set")
+	}
+	suite.configFile1 = filepath.Join(stackDir, "runtime", "config", "firefly_core_0.yml")
+	suite.configFile2 = filepath.Join(stackDir, "runtime", "config", "firefly_core_1.yml")
+}
+
+func (suite *ContractMigrationTestSuite) BeforeTest(suiteName, testName string) {
+	suite.testState = beforeE2ETest(suite.T())
+}
+
+func (suite *ContractMigrationTestSuite) AfterTest(suiteName, testName string) {
+	e2e.VerifyAllOperationsSucceeded(suite.T(), []*client.FireFlyClient{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
+}
+
+func (suite *ContractMigrationTestSuite) TestContractMigration() {
+	defer suite.testState.Done()
+
+	address1 := deployContract(suite.T(), suite.stackName, "firefly/Firefly.json")
+	address2 := deployContract(suite.T(), suite.stackName, "firefly/Firefly.json")
+	runMigrationTest(suite, address1, address2, false)
+}
+
+func runMigrationTest(suite *ContractMigrationTestSuite, address1, address2 string, startOnV1 bool) {
+	testNamespace := randomName(suite.T())
+	suite.T().Logf("Test namespace: %s", testNamespace)
+
+	org := map[string]interface{}{}
+	namespaceInfo := map[string]interface{}{
+		"name": testNamespace,
+		"multiparty": map[string]interface{}{
+			"enabled": true,
+			"org":     org,
+			"contract": []map[string]interface{}{
+				{
+					"location": map[string]interface{}{"address": address1},
+				},
+				{
+					"location": map[string]interface{}{"address": address2},
+				},
+			},
+		},
+	}
+	data := &core.DataRefOrValue{Value: fftypes.JSONAnyPtr(`"test"`)}
+
+	// Add the new namespace to both config files
+	data1 := readConfig(suite.T(), suite.configFile1)
+	org["name"] = suite.testState.org1.Name
+	org["key"] = suite.testState.org1key.Value
+	addNamespace(data1, namespaceInfo)
+	writeConfig(suite.T(), suite.configFile1, data1)
+
+	data2 := readConfig(suite.T(), suite.configFile2)
+	org["name"] = suite.testState.org2.Name
+	org["key"] = suite.testState.org2key.Value
+	addNamespace(data2, namespaceInfo)
+	writeConfig(suite.T(), suite.configFile2, data2)
+
+	admin1 := client.NewResty(suite.T())
+	admin2 := client.NewResty(suite.T())
+	admin1.SetBaseURL(suite.adminHost1 + "/spi/v1")
+	admin2.SetBaseURL(suite.adminHost2 + "/spi/v1")
+
+	// Reset both nodes to pick up the new namespace
+	resetFireFly(suite.T(), admin1)
+	resetFireFly(suite.T(), admin2)
+	e2e.PollForUp(suite.T(), suite.testState.client1)
+	e2e.PollForUp(suite.T(), suite.testState.client2)
+
+	client1 := client.NewFireFly(suite.T(), suite.testState.client1.Hostname, testNamespace)
+	client2 := client.NewFireFly(suite.T(), suite.testState.client2.Hostname, testNamespace)
+
+	eventNames := "message_confirmed|blockchain_event_received"
+	queryString := fmt.Sprintf("namespace=%s&ephemeral&autoack&filter.events=%s", testNamespace, eventNames)
+	received1 := e2e.WsReader(client1.WebSocket(suite.T(), queryString, nil))
+	received2 := e2e.WsReader(client2.WebSocket(suite.T(), queryString, nil))
+
+	if startOnV1 {
+		systemClient1 := client.NewFireFly(suite.T(), suite.testState.client1.Hostname, "ff_system")
+		systemClient2 := client.NewFireFly(suite.T(), suite.testState.client2.Hostname, "ff_system")
+
+		// Register org/node identities on ff_system if not registered (but not on the new namespace)
+		if systemClient1.GetOrganization(suite.T(), suite.testState.org1.Name) == nil {
+			systemClient1.RegisterSelfOrg(suite.T(), true)
+			systemClient1.RegisterSelfNode(suite.T(), true)
+		}
+		if systemClient2.GetOrganization(suite.T(), suite.testState.org2.Name) == nil {
+			systemClient2.RegisterSelfOrg(suite.T(), true)
+			systemClient2.RegisterSelfNode(suite.T(), true)
+		}
+
+		// Verify that a broadcast on the new namespace succeeds under the first contract
+		resp, err := client1.BroadcastMessage(suite.T(), "topic", data, false)
+		require.NoError(suite.T(), err)
+		assert.Equal(suite.T(), 202, resp.StatusCode())
+		e2e.WaitForMessageConfirmed(suite.T(), received1, core.MessageTypeBroadcast)
+		e2e.WaitForMessageConfirmed(suite.T(), received2, core.MessageTypeBroadcast)
+	}
+
+	// Register org/node identities on the new namespace
+	client1.RegisterSelfOrg(suite.T(), true)
+	client1.RegisterSelfNode(suite.T(), true)
+	client2.RegisterSelfOrg(suite.T(), true)
+	client2.RegisterSelfNode(suite.T(), true)
+
+	// Migrate to the new contract
+	client1.NetworkAction(suite.T(), core.NetworkActionTerminate)
+	e2e.WaitForContractEvent(suite.T(), client1, received1, map[string]interface{}{
+		"output": map[string]interface{}{
+			"namespace": "firefly:terminate",
+		},
+	})
+	e2e.WaitForContractEvent(suite.T(), client2, received2, map[string]interface{}{
+		"output": map[string]interface{}{
+			"namespace": "firefly:terminate",
+		},
+	})
+
+	// Verify that a broadcast on the new namespace succeeds under the second contract
+	resp, err := client1.BroadcastMessage(suite.T(), "topic", data, false)
+	require.NoError(suite.T(), err)
+	assert.Equal(suite.T(), 202, resp.StatusCode())
+	e2e.WaitForMessageConfirmed(suite.T(), received1, core.MessageTypeBroadcast)
+	e2e.WaitForMessageConfirmed(suite.T(), received2, core.MessageTypeBroadcast)
+
+	// Verify the contract addresses for both blockchain events
+	events := client1.GetBlockchainEvents(suite.T(), suite.testState.startTime)
+	assert.Equal(suite.T(), address2, strings.ToLower(events[0].Info["address"].(string)))
+	assert.Equal(suite.T(), address1, strings.ToLower(events[1].Info["address"].(string)))
+}
diff --git a/test/e2e/multiparty/contract_migration_v1.go b/test/e2e/multiparty/contract_migration_v1.go
new file mode 100644
index 000000000..e069de972
--- /dev/null
+++ b/test/e2e/multiparty/contract_migration_v1.go
@@ -0,0 +1,70 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package multiparty
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/hyperledger/firefly/test/e2e"
+	"github.com/hyperledger/firefly/test/e2e/client"
+)
+
+type ContractMigrationV1TestSuite struct {
+	ContractMigrationTestSuite
+}
+
+func (suite *ContractMigrationV1TestSuite) SetupSuite() {
+	suite.testState = beforeE2ETest(suite.T())
+	stack := e2e.ReadStack(suite.T())
+	suite.stackName = stack.Name
+
+	adminProtocol1 := schemeHTTP
+	if stack.Members[0].UseHTTPS {
+		adminProtocol1 = schemeHTTPS
+	}
+	adminProtocol2 := schemeHTTP
+	if stack.Members[1].UseHTTPS {
+		adminProtocol2 = schemeHTTPS
+	}
+	suite.adminHost1 = fmt.Sprintf("%s://%s:%d", adminProtocol1, stack.Members[0].FireflyHostname, stack.Members[0].ExposedAdminPort)
+	suite.adminHost2 = fmt.Sprintf("%s://%s:%d", adminProtocol2, stack.Members[1].FireflyHostname, stack.Members[1].ExposedAdminPort)
+
+	stackDir := os.Getenv("STACK_DIR")
+	if stackDir == "" {
+		suite.T().Fatal("STACK_DIR must be set")
+	}
+	suite.configFile1 = filepath.Join(stackDir, "runtime", "config", "firefly_core_0.yml")
+	suite.configFile2 = filepath.Join(stackDir, "runtime", "config", "firefly_core_1.yml")
+}
+
+func (suite *ContractMigrationV1TestSuite) BeforeTest(suiteName, testName string) {
+	suite.testState = beforeE2ETest(suite.T())
+}
+
+func (suite *ContractMigrationV1TestSuite) AfterTest(suiteName, testName string) {
+	e2e.VerifyAllOperationsSucceeded(suite.T(), []*client.FireFlyClient{suite.testState.client1, suite.testState.client2}, suite.testState.startTime)
+}
+
+func (suite *ContractMigrationV1TestSuite) TestContractMigration() {
+	defer suite.testState.Done()
+
+	address1 := deployContract(suite.T(), suite.stackName, "firefly/FireflyV1.json")
+	address2 := deployContract(suite.T(), suite.stackName, "firefly/Firefly.json")
+	runMigrationTest(&suite.ContractMigrationTestSuite, address1, address2, true)
+}
diff --git a/test/e2e/multiparty/ethereum_contract_test.go b/test/e2e/multiparty/ethereum_contracts.go
similarity index 95%
rename from test/e2e/multiparty/ethereum_contract_test.go
rename to test/e2e/multiparty/ethereum_contracts.go
index 8613a2323..3b3633ecd 100644
--- a/test/e2e/multiparty/ethereum_contract_test.go
+++ b/test/e2e/multiparty/ethereum_contracts.go
@@ -35,22 +35,6 @@ import (
 
 var contractVersion, _ = nanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", nanoid.DefaultSize)
 
-type uploadABIResult struct {
-	ID string `json:"id"`
-}
-
-type deployABIResult struct {
-	ContractAddress string `json:"contractAddress"`
-}
-
-type ethconnectOutput struct {
-	Output string `json:"output"`
-}
-
-type simpleStorageBody struct {
-	NewValue string `json:"newValue"`
-}
-
 func simpleStorageFFIChanged() *fftypes.FFIEvent {
 	return &fftypes.FFIEvent{
 		FFIEventDefinition: fftypes.FFIEventDefinition{
@@ -110,7 +94,8 @@ func simpleStorageFFIGet() *fftypes.FFIMethod {
 }
 
 func deployContract(t *testing.T, stackName, contract string) string {
-	out, err := exec.Command("ff", "deploy", "ethereum", stackName, "../../data/contracts/"+contract).Output()
+	path := "../../data/contracts/" + contract
+	out, err := exec.Command("ff", "deploy", "ethereum", stackName, path).Output()
 	require.NoError(t, err)
 	var output map[string]interface{}
 	err = json.Unmarshal(out, &output)
@@ -154,7 +139,7 @@ func (suite *EthereumContractTestSuite) AfterTest(suiteName, testName string) {
 func (suite *EthereumContractTestSuite) TestDirectInvokeMethod() {
 	defer suite.testState.Done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, true)
+	received1 := e2e.WsReader(suite.testState.ws1)
 	listener := suite.testState.client1.CreateContractListener(suite.T(), simpleStorageFFIChanged(), &fftypes.JSONObject{
 		"address": suite.contractAddress,
 	})
@@ -208,7 +193,7 @@ func (suite *EthereumContractTestSuite) TestDirectInvokeMethod() {
 func (suite *EthereumContractTestSuite) TestFFIInvokeMethod() {
 	defer suite.testState.Done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, true)
+	received1 := e2e.WsReader(suite.testState.ws1)
 
 	ffiReference := &fftypes.FFIReference{
 		ID: suite.interfaceID,
@@ -267,7 +252,7 @@ func (suite *EthereumContractTestSuite) TestFFIInvokeMethod() {
 func (suite *EthereumContractTestSuite) TestContractAPIMethod() {
 	defer suite.testState.Done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, true)
+	received1 := e2e.WsReader(suite.testState.ws1)
 	APIName := fftypes.NewUUID().String()
 
 	ffiReference := &fftypes.FFIReference{
diff --git a/test/e2e/gateway/ethereum_test.go b/test/e2e/multiparty/ethereum_v1_migration.go
similarity index 82%
rename from test/e2e/gateway/ethereum_test.go
rename to test/e2e/multiparty/ethereum_v1_migration.go
index 7ce541258..2e628e763 100644
--- a/test/e2e/gateway/ethereum_test.go
+++ b/test/e2e/multiparty/ethereum_v1_migration.go
@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package gateway
+package multiparty
 
 import (
 	"testing"
@@ -22,7 +22,6 @@ import (
 	"github.com/stretchr/testify/suite"
 )
 
-func TestEthereumGatewayE2ESuite(t *testing.T) {
-	suite.Run(t, new(TokensTestSuite))
-	suite.Run(t, new(EthereumContractTestSuite))
+func TestEthereumMultipartyV1MigrationE2ESuite(t *testing.T) {
+	suite.Run(t, new(ContractMigrationV1TestSuite))
 }
diff --git a/test/e2e/multiparty/fabric_contract_test.go b/test/e2e/multiparty/fabric_contracts.go
similarity index 84%
rename from test/e2e/multiparty/fabric_contract_test.go
rename to test/e2e/multiparty/fabric_contracts.go
index 572da8a1e..5677877c3 100644
--- a/test/e2e/multiparty/fabric_contract_test.go
+++ b/test/e2e/multiparty/fabric_contracts.go
@@ -34,19 +34,6 @@ import (
 	"github.com/stretchr/testify/suite"
 )
 
-type fabConnectHeaders struct {
-	Type      string `json:"type"`
-	Channel   string `json:"channel"`
-	Signer    string `json:"signer"`
-	Chaincode string `json:"chaincode"`
-}
-
-type createAssetBody struct {
-	Headers fabConnectHeaders `json:"headers"`
-	Func    string            `json:"func"`
-	Args    []string          `json:"args"`
-}
-
 var assetCreatedEvent = &fftypes.FFIEvent{
 	FFIEventDefinition: fftypes.FFIEventDefinition{
 		Name: "AssetCreated",
@@ -101,26 +88,6 @@ func deployChaincode(t *testing.T, stackName string) string {
 	return chaincodeName
 }
 
-func invokeFabContract(t *testing.T, client *resty.Client, channel, chaincode, signer, method string, args []string) {
-	path := "/transactions"
-	body := &createAssetBody{
-		Headers: fabConnectHeaders{
-			Type:      "SendTransaction",
-			Channel:   channel,
-			Signer:    signer,
-			Chaincode: chaincode,
-		},
-		Func: method,
-		Args: args,
-	}
-	resp, err := client.R().
-		SetHeader("x-firefly-sync", "true").
-		SetBody(body).
-		Post(path)
-	require.NoError(t, err)
-	require.Equal(t, 200, resp.StatusCode(), "POST %s [%d]: %s", path, resp.StatusCode(), resp.String())
-}
-
 type FabricContractTestSuite struct {
 	suite.Suite
 	testState     *testState
@@ -147,7 +114,7 @@ func (suite *FabricContractTestSuite) AfterTest(suiteName, testName string) {
 func (suite *FabricContractTestSuite) TestE2EContractEvents() {
 	defer suite.testState.done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, true)
+	received1 := e2e.WsReader(suite.testState.ws1)
 
 	sub := suite.testState.client1.CreateContractListener(suite.T(), assetCreatedEvent, &fftypes.JSONObject{
 		"channel":   "firefly",
diff --git a/test/e2e/multiparty/identity_test.go b/test/e2e/multiparty/identity.go
similarity index 94%
rename from test/e2e/multiparty/identity_test.go
rename to test/e2e/multiparty/identity.go
index 162e14a33..3e7efdee9 100644
--- a/test/e2e/multiparty/identity_test.go
+++ b/test/e2e/multiparty/identity.go
@@ -1,4 +1,4 @@
-// Copyright © 2021 Kaleido, Inc.
+// Copyright © 2022 Kaleido, Inc.
 //
 // SPDX-License-Identifier: Apache-2.0
 //
@@ -48,8 +48,8 @@ func (suite *IdentityTestSuite) TestCustomChildIdentityBroadcasts() {
 	defer suite.testState.done()
 
 	ctx := context.Background()
-	received1 := e2e.WsReader(suite.testState.ws1, false)
-	received2 := e2e.WsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1)
+	received2 := e2e.WsReader(suite.testState.ws2)
 
 	totalIdentities := 2
 	ts := time.Now().Unix()
@@ -101,8 +101,8 @@ func (suite *IdentityTestSuite) TestCustomChildIdentityBroadcasts() {
 func (suite *IdentityTestSuite) TestCustomChildIdentityPrivate() {
 	defer suite.testState.done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, false)
-	received2 := e2e.WsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1)
+	received2 := e2e.WsReader(suite.testState.ws2)
 
 	org1key := getUnregisteredAccount(suite, suite.testState.org1.Name)
 	org2key := getUnregisteredAccount(suite, suite.testState.org2.Name)
@@ -111,14 +111,14 @@ func (suite *IdentityTestSuite) TestCustomChildIdentityPrivate() {
 	custom1 := suite.testState.client1.ClaimCustomIdentity(suite.T(),
 		org1key,
 		fmt.Sprintf("custom_%d_org1priv", ts),
-		fmt.Sprintf("Description org1priv"),
+		"Description org1priv",
 		nil,
 		suite.testState.org1.ID,
 		true)
 	custom2 := suite.testState.client2.ClaimCustomIdentity(suite.T(),
 		org2key,
 		fmt.Sprintf("custom_%d_org2priv", ts),
-		fmt.Sprintf("Description org2priv"),
+		"Description org2priv",
 		nil,
 		suite.testState.org2.ID,
 		true)
diff --git a/test/e2e/multiparty/onchain_offchain_test.go b/test/e2e/multiparty/onchain_offchain.go
similarity index 94%
rename from test/e2e/multiparty/onchain_offchain_test.go
rename to test/e2e/multiparty/onchain_offchain.go
index 016ae0122..8e62a1202 100644
--- a/test/e2e/multiparty/onchain_offchain_test.go
+++ b/test/e2e/multiparty/onchain_offchain.go
@@ -53,8 +53,8 @@ func (suite *OnChainOffChainTestSuite) AfterTest(suiteName, testName string) {
 func (suite *OnChainOffChainTestSuite) TestE2EBroadcast() {
 	defer suite.testState.done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, true)
-	received2 := e2e.WsReader(suite.testState.ws2, true)
+	received1 := e2e.WsReader(suite.testState.ws1)
+	received2 := e2e.WsReader(suite.testState.ws2)
 
 	// Broadcast some messages, that should get batched, across two topics
 	totalMessages := 10
@@ -95,8 +95,8 @@ func (suite *OnChainOffChainTestSuite) TestE2EBroadcast() {
 func (suite *OnChainOffChainTestSuite) TestStrongDatatypesBroadcast() {
 	defer suite.testState.done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, true)
-	received2 := e2e.WsReader(suite.testState.ws2, true)
+	received1 := e2e.WsReader(suite.testState.ws1)
+	received2 := e2e.WsReader(suite.testState.ws2)
 
 	var resp *resty.Response
 	value := fftypes.JSONAnyPtr(`"Hello"`)
@@ -121,7 +121,7 @@ func (suite *OnChainOffChainTestSuite) TestStrongDatatypesBroadcast() {
 		Version: version,
 		Value:   fftypes.JSONAnyPtrBytes(e2e.WidgetSchemaJSON),
 	}
-	dt = suite.testState.client1.CreateDatatype(suite.T(), dt, true)
+	suite.testState.client1.CreateDatatype(suite.T(), dt, true)
 
 	resp, err = suite.testState.client1.BroadcastMessage(suite.T(), "topic1", &data, true)
 	require.NoError(suite.T(), err)
@@ -144,8 +144,8 @@ func (suite *OnChainOffChainTestSuite) TestStrongDatatypesBroadcast() {
 func (suite *OnChainOffChainTestSuite) TestStrongDatatypesPrivate() {
 	defer suite.testState.done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, true)
-	received2 := e2e.WsReader(suite.testState.ws2, true)
+	received1 := e2e.WsReader(suite.testState.ws1)
+	received2 := e2e.WsReader(suite.testState.ws2)
 
 	var resp *resty.Response
 	value := fftypes.JSONAnyPtr(`{"foo":"bar"}`)
@@ -173,7 +173,7 @@ func (suite *OnChainOffChainTestSuite) TestStrongDatatypesPrivate() {
 		Version: version,
 		Value:   fftypes.JSONAnyPtrBytes(e2e.WidgetSchemaJSON),
 	}
-	dt = suite.testState.client1.CreateDatatype(suite.T(), dt, true)
+	suite.testState.client1.CreateDatatype(suite.T(), dt, true)
 
 	resp, err = suite.testState.client1.PrivateMessage("topic1", &data, []string{
 		suite.testState.org1.Name,
@@ -203,8 +203,8 @@ func (suite *OnChainOffChainTestSuite) TestE2EPrivate() {
 
 	defer suite.testState.done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, false)
-	received2 := e2e.WsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1)
+	received2 := e2e.WsReader(suite.testState.ws2)
 
 	// Send 10 messages, that should get batched, across two topics
 	totalMessages := 10
@@ -248,8 +248,8 @@ func (suite *OnChainOffChainTestSuite) TestE2EPrivate() {
 func (suite *OnChainOffChainTestSuite) TestE2EBroadcastBlob() {
 	defer suite.testState.done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, false)
-	received2 := e2e.WsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1)
+	received2 := e2e.WsReader(suite.testState.ws2)
 
 	var resp *resty.Response
 
@@ -274,8 +274,8 @@ func (suite *OnChainOffChainTestSuite) TestE2EBroadcastBlob() {
 func (suite *OnChainOffChainTestSuite) TestE2EPrivateBlobDatatypeTagged() {
 	defer suite.testState.done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, false)
-	received2 := e2e.WsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1)
+	received2 := e2e.WsReader(suite.testState.ws2)
 
 	var resp *resty.Response
 
@@ -304,8 +304,8 @@ func (suite *OnChainOffChainTestSuite) TestE2EPrivateBlobDatatypeTagged() {
 func (suite *OnChainOffChainTestSuite) TestE2EWebhookExchange() {
 	defer suite.testState.done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, false)
-	received2 := e2e.WsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1)
+	received2 := e2e.WsReader(suite.testState.ws2)
 
 	subJSON := fmt.Sprintf(`{
 		"transport": "webhooks",
diff --git a/test/e2e/multiparty/tokens_test.go b/test/e2e/multiparty/tokens.go
similarity index 95%
rename from test/e2e/multiparty/tokens_test.go
rename to test/e2e/multiparty/tokens.go
index d882d4e06..af02d12f2 100644
--- a/test/e2e/multiparty/tokens_test.go
+++ b/test/e2e/multiparty/tokens.go
@@ -18,8 +18,6 @@ package multiparty
 
 import (
 	"fmt"
-	"math/rand"
-	"time"
 
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly/pkg/core"
@@ -52,12 +50,10 @@ func (suite *TokensTestSuite) AfterTest(suiteName, testName string) {
 func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 	defer suite.testState.done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, false)
-	received2 := e2e.WsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1)
+	received2 := e2e.WsReader(suite.testState.ws2)
 
-	pools := suite.testState.client1.GetTokenPools(suite.T(), time.Unix(0, 0))
-	rand.Seed(time.Now().UnixNano())
-	poolName := fmt.Sprintf("pool%d", rand.Intn(10000))
+	poolName := fmt.Sprintf("pool_%s", randomName(suite.T()))
 	suite.T().Logf("Pool name: %s", poolName)
 
 	pool := &core.TokenPool{
@@ -70,7 +66,7 @@ func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 	poolID := poolResp.ID
 
 	e2e.WaitForEvent(suite.T(), received1, core.EventTypePoolConfirmed, poolID)
-	pools = suite.testState.client1.GetTokenPools(suite.T(), suite.testState.startTime)
+	pools := suite.testState.client1.GetTokenPools(suite.T(), suite.testState.startTime)
 	assert.Equal(suite.T(), 1, len(pools))
 	assert.Equal(suite.T(), suite.testState.namespace, pools[0].Namespace)
 	assert.Equal(suite.T(), suite.connector, pools[0].Connector)
@@ -220,12 +216,10 @@ func (suite *TokensTestSuite) TestE2EFungibleTokensAsync() {
 func (suite *TokensTestSuite) TestE2ENonFungibleTokensSync() {
 	defer suite.testState.done()
 
-	received1 := e2e.WsReader(suite.testState.ws1, false)
-	received2 := e2e.WsReader(suite.testState.ws2, false)
+	received1 := e2e.WsReader(suite.testState.ws1)
+	received2 := e2e.WsReader(suite.testState.ws2)
 
-	pools := suite.testState.client1.GetTokenPools(suite.T(), time.Unix(0, 0))
-	rand.Seed(time.Now().UnixNano())
-	poolName := fmt.Sprintf("pool%d", rand.Intn(10000))
+	poolName := fmt.Sprintf("pool_%s", randomName(suite.T()))
 	suite.T().Logf("Pool name: %s", poolName)
 
 	pool := &core.TokenPool{
@@ -244,7 +238,7 @@ func (suite *TokensTestSuite) TestE2ENonFungibleTokensSync() {
 
 	e2e.WaitForEvent(suite.T(), received1, core.EventTypePoolConfirmed, poolID)
 	e2e.WaitForEvent(suite.T(), received2, core.EventTypePoolConfirmed, poolID)
-	pools = suite.testState.client1.GetTokenPools(suite.T(), suite.testState.startTime)
+	pools := suite.testState.client1.GetTokenPools(suite.T(), suite.testState.startTime)
 	assert.Equal(suite.T(), 1, len(pools))
 	assert.Equal(suite.T(), suite.testState.namespace, pools[0].Namespace)
 	assert.Equal(suite.T(), poolName, pools[0].Name)
diff --git a/test/e2e/run.sh b/test/e2e/run.sh
index df912bf70..145e46ff5 100755
--- a/test/e2e/run.sh
+++ b/test/e2e/run.sh
@@ -128,7 +128,7 @@ checkOk $?
 export STACK_DIR
 
 runTest() {
-  go clean -testcache && go test -v -p 1 ./multiparty ./gateway -run $TEST_SUITE
+  go clean -testcache && go test -v -p 1 ./runners -run $TEST_SUITE
   checkOk $?
 }
 runTest
diff --git a/test/e2e/multiparty/ethereum_namespace_test.go b/test/e2e/runners/ethereum_gateway_test.go
similarity index 76%
rename from test/e2e/multiparty/ethereum_namespace_test.go
rename to test/e2e/runners/ethereum_gateway_test.go
index 095251ee8..d0f53cfbb 100644
--- a/test/e2e/multiparty/ethereum_namespace_test.go
+++ b/test/e2e/runners/ethereum_gateway_test.go
@@ -14,16 +14,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package multiparty
+package runners
 
 import (
 	"testing"
 
+	"github.com/hyperledger/firefly/test/e2e/gateway"
 	"github.com/stretchr/testify/suite"
 )
 
-func TestEthereumNamespaceE2ESuite(t *testing.T) {
-	suite.Run(t, new(OnChainOffChainTestSuite))
-	suite.Run(t, new(TokensTestSuite))
-	suite.Run(t, new(EthereumContractTestSuite))
+func TestEthereumGatewayE2ESuite(t *testing.T) {
+	suite.Run(t, new(gateway.TokensTestSuite))
+	suite.Run(t, new(gateway.EthereumContractTestSuite))
 }
diff --git a/test/e2e/runners/ethereum_multiparty_test.go b/test/e2e/runners/ethereum_multiparty_test.go
new file mode 100644
index 000000000..611661234
--- /dev/null
+++ b/test/e2e/runners/ethereum_multiparty_test.go
@@ -0,0 +1,32 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package runners
+
+import (
+	"testing"
+
+	"github.com/hyperledger/firefly/test/e2e/multiparty"
+	"github.com/stretchr/testify/suite"
+)
+
+func TestEthereumMultipartyE2ESuite(t *testing.T) {
+	suite.Run(t, new(multiparty.IdentityTestSuite))
+	suite.Run(t, new(multiparty.OnChainOffChainTestSuite))
+	suite.Run(t, new(multiparty.TokensTestSuite))
+	suite.Run(t, new(multiparty.EthereumContractTestSuite))
+	suite.Run(t, new(multiparty.ContractMigrationTestSuite))
+}
diff --git a/test/e2e/multiparty/ethereum_test.go b/test/e2e/runners/ethereum_namespace.go
similarity index 71%
rename from test/e2e/multiparty/ethereum_test.go
rename to test/e2e/runners/ethereum_namespace.go
index 6aa380d8a..0faa2d7b5 100644
--- a/test/e2e/multiparty/ethereum_test.go
+++ b/test/e2e/runners/ethereum_namespace.go
@@ -14,17 +14,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package multiparty
+package runners
 
 import (
 	"testing"
 
+	"github.com/hyperledger/firefly/test/e2e/multiparty"
 	"github.com/stretchr/testify/suite"
 )
 
-func TestEthereumMultipartyE2ESuite(t *testing.T) {
-	suite.Run(t, new(IdentityTestSuite))
-	suite.Run(t, new(OnChainOffChainTestSuite))
-	suite.Run(t, new(TokensTestSuite))
-	suite.Run(t, new(EthereumContractTestSuite))
+func TestEthereumNamespaceE2ESuite(t *testing.T) {
+	suite.Run(t, new(multiparty.OnChainOffChainTestSuite))
+	suite.Run(t, new(multiparty.TokensTestSuite))
+	suite.Run(t, new(multiparty.EthereumContractTestSuite))
 }
diff --git a/test/e2e/runners/ethereum_v1_migration_test.go b/test/e2e/runners/ethereum_v1_migration_test.go
new file mode 100644
index 000000000..9a09620b2
--- /dev/null
+++ b/test/e2e/runners/ethereum_v1_migration_test.go
@@ -0,0 +1,32 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package runners
+
+import (
+	"testing"
+
+	"github.com/hyperledger/firefly/test/e2e/multiparty"
+	"github.com/stretchr/testify/suite"
+)
+
+// This suite can only be run once per stack.
+// Specifically, it must be run on a stack that has never used a V1 FireFly multiparty contract.
+// The test deploys a V1 contract, then migrates to V2. This is a one-time operation that cannot
+// be performed again on the same stack.
+func TestEthereumV1MigrationE2ESuite(t *testing.T) {
+	suite.Run(t, new(multiparty.ContractMigrationV1TestSuite))
+}
diff --git a/test/e2e/gateway/fabric_test.go b/test/e2e/runners/fabric_gateway_test.go
similarity index 86%
rename from test/e2e/gateway/fabric_test.go
rename to test/e2e/runners/fabric_gateway_test.go
index 0021278b5..e9be00868 100644
--- a/test/e2e/gateway/fabric_test.go
+++ b/test/e2e/runners/fabric_gateway_test.go
@@ -14,14 +14,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package gateway
+package runners
 
 import (
 	"testing"
 
+	"github.com/hyperledger/firefly/test/e2e/gateway"
 	"github.com/stretchr/testify/suite"
 )
 
 func TestFabricGatewayE2ESuite(t *testing.T) {
-	suite.Run(t, new(FabricContractTestSuite))
+	suite.Run(t, new(gateway.FabricContractTestSuite))
 }
diff --git a/test/e2e/multiparty/fabric_test.go b/test/e2e/runners/fabric_multiparty_test.go
similarity index 76%
rename from test/e2e/multiparty/fabric_test.go
rename to test/e2e/runners/fabric_multiparty_test.go
index dfbf6a932..e42349656 100644
--- a/test/e2e/multiparty/fabric_test.go
+++ b/test/e2e/runners/fabric_multiparty_test.go
@@ -14,16 +14,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package multiparty
+package runners
 
 import (
 	"testing"
 
+	"github.com/hyperledger/firefly/test/e2e/multiparty"
 	"github.com/stretchr/testify/suite"
 )
 
 func TestFabricMultipartyE2ESuite(t *testing.T) {
-	suite.Run(t, new(IdentityTestSuite))
-	suite.Run(t, new(OnChainOffChainTestSuite))
-	suite.Run(t, new(FabricContractTestSuite))
+	suite.Run(t, new(multiparty.IdentityTestSuite))
+	suite.Run(t, new(multiparty.OnChainOffChainTestSuite))
+	suite.Run(t, new(multiparty.FabricContractTestSuite))
 }
diff --git a/test/e2e/stack.go b/test/e2e/stack.go
index 75df4e9dc..1d9392194 100644
--- a/test/e2e/stack.go
+++ b/test/e2e/stack.go
@@ -35,6 +35,7 @@ type StackState struct {
 
 type Member struct {
 	ExposedFireflyPort   int         `json:"exposedFireflyPort,omitempty"`
+	ExposedAdminPort     int         `json:"exposedFireflyAdminPort,omitempty"`
 	FireflyHostname      string      `json:"fireflyHostname,omitempty"`
 	Username             string      `json:"username,omitempty"`
 	Password             string      `json:"password,omitempty"`