Repository: hyperledger/firefly
Pull request #895 (2022-07-08 20:39:29)

Map namespace between local name and remote name
Part of [FIR-12](https://github.com/hyperledger/firefly-fir/pull/12)

Orchestrator is now aware of the namespace local name _and_ remote name, as are some of the managers. Plugins are intentionally not aware of the mapping, but are passed the local or remote namespace names as appropriate.

The only two items which _record_ a remote namespace name are `Message` and `Group`. These now have an additional `localNamespace` field, which will be populated with the local namespace, while the existing `namespace` field will be populated with the remote namespace (because it is part of the hash in both of these cases, it must remain the same for all members). In addition, `Batch` and `Data` items will be populated with the remote namespace name for network transit (but will be stored with the local namespace name only). Beyond these, every other existing object simply continues to populate its `namespace` with a local namespace name.

Some more notes on how the local namespace is filled for other items:
* All definition types (datatypes, contract FFIs/APIs, identities, and token pools) are transmitted _without_ a namespace filled. Each receiving node will map the definition based on the enclosing message's namespace, and then will fill in the local namespace name before storing the item.
* Token transfers and approvals continue to inherit their namespace from the associated token pool.
* Contract listeners are still local-only, and blockchain events tied to them will inherit their namespace.


Patch:
diff --git a/Makefile b/Makefile
index dfee557d9..03fedaf65 100644
--- a/Makefile
+++ b/Makefile
@@ -37,6 +37,7 @@ endef
 $(eval $(call makemock, $$(WSCLIENT_PATH),         WSClient,           wsmocks))
 $(eval $(call makemock, pkg/blockchain,            Plugin,             blockchainmocks))
 $(eval $(call makemock, pkg/blockchain,            Callbacks,          blockchainmocks))
+$(eval $(call makemock, pkg/core,                  OperationCallbacks, coremocks))
 $(eval $(call makemock, pkg/database,              Plugin,             databasemocks))
 $(eval $(call makemock, pkg/database,              Callbacks,          databasemocks))
 $(eval $(call makemock, pkg/sharedstorage,         Plugin,             sharedstoragemocks))
diff --git a/db/migrations/postgres/000095_add_message_local_namespace.down.sql b/db/migrations/postgres/000095_add_message_local_namespace.down.sql
new file mode 100644
index 000000000..334f2c840
--- /dev/null
+++ b/db/migrations/postgres/000095_add_message_local_namespace.down.sql
@@ -0,0 +1,4 @@
+BEGIN;
+ALTER TABLE messages DROP COLUMN namespace_local;
+ALTER TABLE groups DROP COLUMN namespace_local;
+COMMIT;
diff --git a/db/migrations/postgres/000095_add_message_local_namespace.up.sql b/db/migrations/postgres/000095_add_message_local_namespace.up.sql
new file mode 100644
index 000000000..c225b9259
--- /dev/null
+++ b/db/migrations/postgres/000095_add_message_local_namespace.up.sql
@@ -0,0 +1,9 @@
+BEGIN;
+ALTER TABLE messages ADD COLUMN namespace_local VARCHAR(64);
+UPDATE messages SET namespace_local = namespace;
+ALTER TABLE messages ALTER COLUMN namespace_local SET NOT NULL;
+
+ALTER TABLE groups ADD COLUMN namespace_local VARCHAR(64);
+UPDATE groups SET namespace_local = namespace;
+ALTER TABLE groups ALTER COLUMN namespace_local SET NOT NULL;
+COMMIT;
diff --git a/db/migrations/sqlite/000095_add_message_local_namespace.down.sql b/db/migrations/sqlite/000095_add_message_local_namespace.down.sql
new file mode 100644
index 000000000..784e7fb27
--- /dev/null
+++ b/db/migrations/sqlite/000095_add_message_local_namespace.down.sql
@@ -0,0 +1,2 @@
+ALTER TABLE messages DROP COLUMN namespace_local;
+ALTER TABLE groups DROP COLUMN namespace_local;
\ No newline at end of file
diff --git a/db/migrations/sqlite/000095_add_message_local_namespace.up.sql b/db/migrations/sqlite/000095_add_message_local_namespace.up.sql
new file mode 100644
index 000000000..d775ee40c
--- /dev/null
+++ b/db/migrations/sqlite/000095_add_message_local_namespace.up.sql
@@ -0,0 +1,5 @@
+ALTER TABLE messages ADD COLUMN namespace_local VARCHAR(64);
+UPDATE messages SET namespace_local = namespace;
+
+ALTER TABLE groups ADD COLUMN namespace_local VARCHAR(64);
+UPDATE groups SET namespace_local = namespace;
diff --git a/docs/reference/types/group.md b/docs/reference/types/group.md
index 97358af27..84876c816 100644
--- a/docs/reference/types/group.md
+++ b/docs/reference/types/group.md
@@ -36,6 +36,7 @@ nav_order: 18
             "node": "61a99af8-c1f7-48ea-8fcc-489e4822a0ed"
         }
     ],
+    "localNamespace": "ns1",
     "message": "0b9dfb76-103d-443d-92fd-b114fe07c54d",
     "hash": "c52ad6c034cf5c7382d9a294f49297096a52eb55cc2da696c564b2a276633b95",
     "created": "2022-05-16T01:23:16Z"
@@ -46,9 +47,10 @@ nav_order: 18
 
 | Field Name | Description | Type |
 |------------|-------------|------|
-| `namespace` | The namespace of the group | `string` |
+| `namespace` | The namespace of the group within the multiparty network | `string` |
 | `name` | The optional name of the group, allowing multiple unique groups to exist with the same list of recipients | `string` |
 | `members` | The list of members in this privacy group | [`Member[]`](#member) |
+| `localNamespace` | The local namespace of the group | `string` |
 | `message` | The message used to broadcast this group privately to the members | [`UUID`](simpletypes#uuid) |
 | `hash` | The identifier hash of this group. Derived from the name and group members | `Bytes32` |
 | `created` | The time when the group was first used to send a message in the network | [`FFTime`](simpletypes#fftime) |
diff --git a/docs/reference/types/message.md b/docs/reference/types/message.md
index 3ce6b3a65..1e2c1c503 100644
--- a/docs/reference/types/message.md
+++ b/docs/reference/types/message.md
@@ -40,6 +40,7 @@ nav_order: 15
         "tag": "blue_message",
         "datahash": "c07be180b147049baced0b6219d9ce7a84ab48f2ca7ca7ae949abb3fe6491b54"
     },
+    "localNamespace": "ns1",
     "state": "confirmed",
     "confirmed": "2022-05-16T01:23:16Z",
     "data": [
@@ -56,6 +57,7 @@ nav_order: 15
 | Field Name | Description | Type |
 |------------|-------------|------|
 | `header` | The message header contains all fields that are used to build the message hash | [`MessageHeader`](#messageheader) |
+| `localNamespace` | The local namespace of the message | `string` |
 | `hash` | The hash of the message. Derived from the header, which includes the data hash | `Bytes32` |
 | `batch` | The UUID of the batch in which the message was pinned/transferred | [`UUID`](simpletypes#uuid) |
 | `state` | The current state of the message | `FFEnum`:<br/>`"staged"`<br/>`"ready"`<br/>`"sent"`<br/>`"pending"`<br/>`"confirmed"`<br/>`"rejected"` |
@@ -74,7 +76,7 @@ nav_order: 15
 | `author` | The DID of identity of the submitter | `string` |
 | `key` | The on-chain signing key used to sign the transaction | `string` |
 | `created` | The creation time of the message | [`FFTime`](simpletypes#fftime) |
-| `namespace` | The namespace of the message | `string` |
+| `namespace` | The namespace of the message within the multiparty network | `string` |
 | `topics` | A message topic associates this message with an ordered stream of data. A custom topic should be assigned - using the default topic is discouraged | `string[]` |
 | `tag` | The message tag indicates the purpose of the message to the applications that process it | `string` |
 | `datahash` | A single hash representing all data in the message. Derived from the array of data ids+hashes attached to this message | `Bytes32` |
diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml
index 4f83b36c3..34e99c65d 100644
--- a/docs/swagger/swagger.yaml
+++ b/docs/swagger/swagger.yaml
@@ -4821,7 +4821,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -4861,6 +4862,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
@@ -5610,6 +5614,9 @@ paths:
                         the name and group members
                       format: byte
                       type: string
+                    localNamespace:
+                      description: The local namespace of the group
+                      type: string
                     members:
                       description: The list of members in this privacy group
                       items:
@@ -5635,7 +5642,8 @@ paths:
                         unique groups to exist with the same list of recipients
                       type: string
                     namespace:
-                      description: The namespace of the group
+                      description: The namespace of the group within the multiparty
+                        network
                       type: string
                   type: object
                 type: array
@@ -5678,6 +5686,9 @@ paths:
                       name and group members
                     format: byte
                     type: string
+                  localNamespace:
+                    description: The local namespace of the group
+                    type: string
                   members:
                     description: The list of members in this privacy group
                     items:
@@ -5703,7 +5714,8 @@ paths:
                       unique groups to exist with the same list of recipients
                     type: string
                   namespace:
-                    description: The namespace of the group
+                    description: The namespace of the group within the multiparty
+                      network
                     type: string
                 type: object
           description: Success
@@ -6954,7 +6966,8 @@ paths:
                           description: The on-chain signing key used to sign the transaction
                           type: string
                         namespace:
-                          description: The namespace of the message
+                          description: The namespace of the message within the multiparty
+                            network
                           type: string
                         tag:
                           description: The message tag indicates the purpose of the
@@ -6994,6 +7007,9 @@ paths:
                           - transfer_private
                           type: string
                       type: object
+                    localNamespace:
+                      description: The local namespace of the message
+                      type: string
                     pins:
                       description: For private messages, a unique pin hash:nonce is
                         assigned for each topic
@@ -7057,10 +7073,51 @@ paths:
                     format: date-time
                     type: string
                   data:
-                    description: The list of data elements attached to the message
+                    description: For input allows you to specify data in-line in the
+                      message, that will be turned into data attachments. For output
+                      when fetchdata is used on API calls, includes the in-line data
+                      payloads of all data attachments
                     items:
-                      description: The list of data elements attached to the message
+                      description: For input allows you to specify data in-line in
+                        the message, that will be turned into data attachments. For
+                        output when fetchdata is used on API calls, includes the in-line
+                        data payloads of all data attachments
                       properties:
+                        blob:
+                          description: An optional in-line hash reference to a previously
+                            uploaded binary data blob
+                          properties:
+                            hash:
+                              description: The hash of the binary blob data
+                              format: byte
+                              type: string
+                            name:
+                              description: The name field from the metadata attached
+                                to the blob, commonly used as a path/filename, and
+                                indexed for search
+                              type: string
+                            public:
+                              description: If this data has been published to shared
+                                storage, this field is the id of the data in the shared
+                                storage plugin (IPFS hash etc.)
+                              type: string
+                            size:
+                              description: The size of the binary data
+                              format: int64
+                              type: integer
+                          type: object
+                        datatype:
+                          description: The optional datatype to use for validation
+                            of the in-line data
+                          properties:
+                            name:
+                              description: The name of the datatype
+                              type: string
+                            version:
+                              description: The version of the datatype. Semantic versioning
+                                is encouraged, such as v1.0.1
+                              type: string
+                          type: object
                         hash:
                           description: The hash of the referenced data
                           format: byte
@@ -7069,6 +7126,13 @@ paths:
                           description: The UUID of the referenced data resource
                           format: uuid
                           type: string
+                        validator:
+                          description: The data validator type to use for in-line
+                            data
+                          type: string
+                        value:
+                          description: The in-line value for the data. Can be any
+                            JSON type - object, array, string, number or boolean
                       type: object
                     type: array
                   group:
@@ -7144,7 +7208,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -7184,6 +7249,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
@@ -7718,7 +7786,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -7758,6 +7827,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
@@ -7841,7 +7913,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -7881,6 +7954,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
@@ -8124,7 +8200,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -8164,6 +8241,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
@@ -8253,7 +8333,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -8293,6 +8374,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
@@ -8477,10 +8561,51 @@ paths:
                     format: date-time
                     type: string
                   data:
-                    description: The list of data elements attached to the message
+                    description: For input allows you to specify data in-line in the
+                      message, that will be turned into data attachments. For output
+                      when fetchdata is used on API calls, includes the in-line data
+                      payloads of all data attachments
                     items:
-                      description: The list of data elements attached to the message
+                      description: For input allows you to specify data in-line in
+                        the message, that will be turned into data attachments. For
+                        output when fetchdata is used on API calls, includes the in-line
+                        data payloads of all data attachments
                       properties:
+                        blob:
+                          description: An optional in-line hash reference to a previously
+                            uploaded binary data blob
+                          properties:
+                            hash:
+                              description: The hash of the binary blob data
+                              format: byte
+                              type: string
+                            name:
+                              description: The name field from the metadata attached
+                                to the blob, commonly used as a path/filename, and
+                                indexed for search
+                              type: string
+                            public:
+                              description: If this data has been published to shared
+                                storage, this field is the id of the data in the shared
+                                storage plugin (IPFS hash etc.)
+                              type: string
+                            size:
+                              description: The size of the binary data
+                              format: int64
+                              type: integer
+                          type: object
+                        datatype:
+                          description: The optional datatype to use for validation
+                            of the in-line data
+                          properties:
+                            name:
+                              description: The name of the datatype
+                              type: string
+                            version:
+                              description: The version of the datatype. Semantic versioning
+                                is encouraged, such as v1.0.1
+                              type: string
+                          type: object
                         hash:
                           description: The hash of the referenced data
                           format: byte
@@ -8489,6 +8614,13 @@ paths:
                           description: The UUID of the referenced data resource
                           format: uuid
                           type: string
+                        validator:
+                          description: The data validator type to use for in-line
+                            data
+                          type: string
+                        value:
+                          description: The in-line value for the data. Can be any
+                            JSON type - object, array, string, number or boolean
                       type: object
                     type: array
                   group:
@@ -8564,7 +8696,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -8604,6 +8737,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
@@ -14075,7 +14211,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -14115,6 +14252,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
@@ -14906,6 +15046,9 @@ paths:
                         the name and group members
                       format: byte
                       type: string
+                    localNamespace:
+                      description: The local namespace of the group
+                      type: string
                     members:
                       description: The list of members in this privacy group
                       items:
@@ -14931,7 +15074,8 @@ paths:
                         unique groups to exist with the same list of recipients
                       type: string
                     namespace:
-                      description: The namespace of the group
+                      description: The namespace of the group within the multiparty
+                        network
                       type: string
                   type: object
                 type: array
@@ -14981,6 +15125,9 @@ paths:
                       name and group members
                     format: byte
                     type: string
+                  localNamespace:
+                    description: The local namespace of the group
+                    type: string
                   members:
                     description: The list of members in this privacy group
                     items:
@@ -15006,7 +15153,8 @@ paths:
                       unique groups to exist with the same list of recipients
                     type: string
                   namespace:
-                    description: The namespace of the group
+                    description: The namespace of the group within the multiparty
+                      network
                     type: string
                 type: object
           description: Success
@@ -16313,7 +16461,8 @@ paths:
                           description: The on-chain signing key used to sign the transaction
                           type: string
                         namespace:
-                          description: The namespace of the message
+                          description: The namespace of the message within the multiparty
+                            network
                           type: string
                         tag:
                           description: The message tag indicates the purpose of the
@@ -16353,6 +16502,9 @@ paths:
                           - transfer_private
                           type: string
                       type: object
+                    localNamespace:
+                      description: The local namespace of the message
+                      type: string
                     pins:
                       description: For private messages, a unique pin hash:nonce is
                         assigned for each topic
@@ -16423,10 +16575,51 @@ paths:
                     format: date-time
                     type: string
                   data:
-                    description: The list of data elements attached to the message
+                    description: For input allows you to specify data in-line in the
+                      message, that will be turned into data attachments. For output
+                      when fetchdata is used on API calls, includes the in-line data
+                      payloads of all data attachments
                     items:
-                      description: The list of data elements attached to the message
+                      description: For input allows you to specify data in-line in
+                        the message, that will be turned into data attachments. For
+                        output when fetchdata is used on API calls, includes the in-line
+                        data payloads of all data attachments
                       properties:
+                        blob:
+                          description: An optional in-line hash reference to a previously
+                            uploaded binary data blob
+                          properties:
+                            hash:
+                              description: The hash of the binary blob data
+                              format: byte
+                              type: string
+                            name:
+                              description: The name field from the metadata attached
+                                to the blob, commonly used as a path/filename, and
+                                indexed for search
+                              type: string
+                            public:
+                              description: If this data has been published to shared
+                                storage, this field is the id of the data in the shared
+                                storage plugin (IPFS hash etc.)
+                              type: string
+                            size:
+                              description: The size of the binary data
+                              format: int64
+                              type: integer
+                          type: object
+                        datatype:
+                          description: The optional datatype to use for validation
+                            of the in-line data
+                          properties:
+                            name:
+                              description: The name of the datatype
+                              type: string
+                            version:
+                              description: The version of the datatype. Semantic versioning
+                                is encouraged, such as v1.0.1
+                              type: string
+                          type: object
                         hash:
                           description: The hash of the referenced data
                           format: byte
@@ -16435,6 +16628,13 @@ paths:
                           description: The UUID of the referenced data resource
                           format: uuid
                           type: string
+                        validator:
+                          description: The data validator type to use for in-line
+                            data
+                          type: string
+                        value:
+                          description: The in-line value for the data. Can be any
+                            JSON type - object, array, string, number or boolean
                       type: object
                     type: array
                   group:
@@ -16510,7 +16710,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -16550,6 +16751,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
@@ -17156,7 +17360,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -17196,6 +17401,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
@@ -17285,7 +17493,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -17325,6 +17534,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
@@ -17575,7 +17787,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -17615,6 +17828,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
@@ -17704,7 +17920,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -17744,6 +17961,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
@@ -17935,10 +18155,51 @@ paths:
                     format: date-time
                     type: string
                   data:
-                    description: The list of data elements attached to the message
+                    description: For input allows you to specify data in-line in the
+                      message, that will be turned into data attachments. For output
+                      when fetchdata is used on API calls, includes the in-line data
+                      payloads of all data attachments
                     items:
-                      description: The list of data elements attached to the message
+                      description: For input allows you to specify data in-line in
+                        the message, that will be turned into data attachments. For
+                        output when fetchdata is used on API calls, includes the in-line
+                        data payloads of all data attachments
                       properties:
+                        blob:
+                          description: An optional in-line hash reference to a previously
+                            uploaded binary data blob
+                          properties:
+                            hash:
+                              description: The hash of the binary blob data
+                              format: byte
+                              type: string
+                            name:
+                              description: The name field from the metadata attached
+                                to the blob, commonly used as a path/filename, and
+                                indexed for search
+                              type: string
+                            public:
+                              description: If this data has been published to shared
+                                storage, this field is the id of the data in the shared
+                                storage plugin (IPFS hash etc.)
+                              type: string
+                            size:
+                              description: The size of the binary data
+                              format: int64
+                              type: integer
+                          type: object
+                        datatype:
+                          description: The optional datatype to use for validation
+                            of the in-line data
+                          properties:
+                            name:
+                              description: The name of the datatype
+                              type: string
+                            version:
+                              description: The version of the datatype. Semantic versioning
+                                is encouraged, such as v1.0.1
+                              type: string
+                          type: object
                         hash:
                           description: The hash of the referenced data
                           format: byte
@@ -17947,6 +18208,13 @@ paths:
                           description: The UUID of the referenced data resource
                           format: uuid
                           type: string
+                        validator:
+                          description: The data validator type to use for in-line
+                            data
+                          type: string
+                        value:
+                          description: The in-line value for the data. Can be any
+                            JSON type - object, array, string, number or boolean
                       type: object
                     type: array
                   group:
@@ -18022,7 +18290,8 @@ paths:
                         description: The on-chain signing key used to sign the transaction
                         type: string
                       namespace:
-                        description: The namespace of the message
+                        description: The namespace of the message within the multiparty
+                          network
                         type: string
                       tag:
                         description: The message tag indicates the purpose of the
@@ -18062,6 +18331,9 @@ paths:
                         - transfer_private
                         type: string
                     type: object
+                  localNamespace:
+                    description: The local namespace of the message
+                    type: string
                   pins:
                     description: For private messages, a unique pin hash:nonce is
                       assigned for each topic
diff --git a/internal/batch/batch_manager.go b/internal/batch/batch_manager.go
index cbba2c421..3c022dd06 100644
--- a/internal/batch/batch_manager.go
+++ b/internal/batch/batch_manager.go
@@ -134,8 +134,8 @@ type dispatcher struct {
 	options    DispatcherOptions
 }
 
-func (bm *batchManager) getProcessorKey(namespace string, identity *core.SignerRef, groupID *fftypes.Bytes32) string {
-	return fmt.Sprintf("%s|%s|%v", namespace, identity.Author, groupID)
+func (bm *batchManager) getProcessorKey(identity *core.SignerRef, groupID *fftypes.Bytes32) string {
+	return fmt.Sprintf("%s|%v", identity.Author, groupID)
 }
 
 func (bm *batchManager) getDispatcherKey(txType core.TransactionType, msgType core.MessageType) string {
@@ -169,7 +169,7 @@ func (bm *batchManager) NewMessages() chan<- int64 {
 	return bm.newMessages
 }
 
-func (bm *batchManager) getProcessor(txType core.TransactionType, msgType core.MessageType, group *fftypes.Bytes32, namespace string, signer *core.SignerRef) (*batchProcessor, error) {
+func (bm *batchManager) getProcessor(txType core.TransactionType, msgType core.MessageType, group *fftypes.Bytes32, signer *core.SignerRef) (*batchProcessor, error) {
 	bm.dispatcherMux.Lock()
 	defer bm.dispatcherMux.Unlock()
 
@@ -178,7 +178,7 @@ func (bm *batchManager) getProcessor(txType core.TransactionType, msgType core.M
 	if !ok {
 		return nil, i18n.NewError(bm.ctx, coremsgs.MsgUnregisteredBatchType, dispatcherKey)
 	}
-	name := bm.getProcessorKey(namespace, signer, group)
+	name := bm.getProcessorKey(signer, group)
 	processor, ok := dispatcher.processors[name]
 	if !ok {
 		processor = newBatchProcessor(
@@ -188,7 +188,6 @@ func (bm *batchManager) getProcessor(txType core.TransactionType, msgType core.M
 				name:              name,
 				txType:            txType,
 				dispatcherName:    dispatcher.name,
-				namespace:         namespace,
 				signer:            *signer,
 				group:             group,
 				dispatch:          dispatcher.handler,
@@ -318,7 +317,7 @@ func (bm *batchManager) messageSequencer() {
 				// the database store. Meaning we cannot rely on the sequence having been set.
 				msg.Sequence = entry.Sequence
 
-				processor, err := bm.getProcessor(msg.Header.TxType, msg.Header.Type, msg.Header.Group, msg.Header.Namespace, &msg.Header.SignerRef)
+				processor, err := bm.getProcessor(msg.Header.TxType, msg.Header.Type, msg.Header.Group, &msg.Header.SignerRef)
 				if err != nil {
 					l.Errorf("Failed to dispatch message %s: %s", msg.Header.ID, err)
 					continue
diff --git a/internal/batch/batch_processor.go b/internal/batch/batch_processor.go
index 0e088451d..0653815bf 100644
--- a/internal/batch/batch_processor.go
+++ b/internal/batch/batch_processor.go
@@ -47,7 +47,6 @@ type batchProcessorConf struct {
 	name           string
 	dispatcherName string
 	txType         core.TransactionType
-	namespace      string
 	signer         core.SignerRef
 	group          *fftypes.Bytes32
 	dispatch       DispatchHandler
@@ -379,11 +378,11 @@ func (bp *batchProcessor) initFlushState(id *fftypes.UUID, flushWork []*batchWor
 			BatchHeader: core.BatchHeader{
 				ID:        id,
 				Type:      bp.conf.DispatcherOptions.BatchType,
-				Namespace: bp.conf.namespace,
+				Namespace: bp.bm.namespace,
 				SignerRef: bp.conf.signer,
 				Group:     bp.conf.group,
 				Created:   fftypes.Now(),
-				Node:      bp.ni.GetNodeUUID(bp.ctx, bp.conf.namespace),
+				Node:      bp.ni.GetNodeUUID(bp.ctx),
 			},
 		},
 	}
diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go
index 637e846f2..22dcbb8b9 100644
--- a/internal/blockchain/ethereum/ethereum.go
+++ b/internal/blockchain/ethereum/ethereum.go
@@ -69,13 +69,14 @@ type subscriptionInfo struct {
 }
 
 type callbacks struct {
-	handlers map[string]blockchain.Callbacks
+	handlers   map[string]blockchain.Callbacks
+	opHandlers map[string]core.OperationCallbacks
 }
 
-func (cb *callbacks) BlockchainOpUpdate(ctx context.Context, plugin blockchain.Plugin, nsOpID string, txState blockchain.TransactionStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) {
+func (cb *callbacks) OperationUpdate(ctx context.Context, plugin blockchain.Plugin, nsOpID string, status core.OpStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) {
 	namespace, _, _ := core.ParseNamespacedOpID(ctx, nsOpID)
-	if handler, ok := cb.handlers[namespace]; ok {
-		handler.BlockchainOpUpdate(plugin, nsOpID, txState, blockchainTXID, errorMessage, opOutput)
+	if handler, ok := cb.opHandlers[namespace]; ok {
+		handler.OperationUpdate(plugin, nsOpID, status, blockchainTXID, errorMessage, opOutput)
 		return
 	}
 	log.L(ctx).Errorf("No handler found for blockchain operation '%s'", nsOpID)
@@ -182,6 +183,7 @@ func (e *Ethereum) Init(ctx context.Context, config config.Section, metrics metr
 	e.metrics = metrics
 	e.capabilities = &blockchain.Capabilities{}
 	e.callbacks.handlers = make(map[string]blockchain.Callbacks)
+	e.callbacks.opHandlers = make(map[string]core.OperationCallbacks)
 
 	if addressResolverConf.GetString(AddressResolverURLTemplate) != "" {
 		if e.addressResolver, err = newAddressResolver(ctx, addressResolverConf); err != nil {
@@ -235,6 +237,10 @@ func (e *Ethereum) SetHandler(namespace string, handler blockchain.Callbacks) {
 	e.callbacks.handlers[namespace] = handler
 }
 
+func (e *Ethereum) SetOperationHandler(namespace string, handler core.OperationCallbacks) {
+	e.callbacks.opHandlers[namespace] = handler
+}
+
 func (e *Ethereum) Start() (err error) {
 	return e.wsconn.Connect()
 }
@@ -473,8 +479,8 @@ func (e *Ethereum) handleReceipt(ctx context.Context, reply fftypes.JSONObject)
 	if replyType != "TransactionSuccess" {
 		updateType = core.OpStatusFailed
 	}
-	l.Infof("Ethconnect '%s' reply: request=%s tx=%s message=%s", replyType, requestID, txHash, message)
-	e.callbacks.BlockchainOpUpdate(ctx, e, requestID, updateType, txHash, message, reply)
+	l.Infof("Received operation update: status=%s request=%s tx=%s message=%s", updateType, requestID, txHash, message)
+	e.callbacks.OperationUpdate(ctx, e, requestID, updateType, txHash, message, reply)
 }
 
 func (e *Ethereum) buildEventLocationString(msgJSON fftypes.JSONObject) string {
diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go
index 59b7eb272..ba1a6e813 100644
--- a/internal/blockchain/fabric/fabric.go
+++ b/internal/blockchain/fabric/fabric.go
@@ -69,13 +69,14 @@ type subscriptionInfo struct {
 }
 
 type callbacks struct {
-	handlers map[string]blockchain.Callbacks
+	handlers   map[string]blockchain.Callbacks
+	opHandlers map[string]core.OperationCallbacks
 }
 
-func (cb *callbacks) BlockchainOpUpdate(ctx context.Context, plugin blockchain.Plugin, nsOpID string, txState blockchain.TransactionStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) {
+func (cb *callbacks) OperationUpdate(ctx context.Context, plugin blockchain.Plugin, nsOpID string, status core.OpStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) {
 	namespace, _, _ := core.ParseNamespacedOpID(ctx, nsOpID)
-	if handler, ok := cb.handlers[namespace]; ok {
-		handler.BlockchainOpUpdate(plugin, nsOpID, txState, blockchainTXID, errorMessage, opOutput)
+	if handler, ok := cb.opHandlers[namespace]; ok {
+		handler.OperationUpdate(plugin, nsOpID, status, blockchainTXID, errorMessage, opOutput)
 	} else {
 		log.L(ctx).Errorf("No handler found for blockchain operation '%s'", nsOpID)
 	}
@@ -223,6 +224,7 @@ func (f *Fabric) Init(ctx context.Context, config config.Section, metrics metric
 	f.metrics = metrics
 	f.capabilities = &blockchain.Capabilities{}
 	f.callbacks.handlers = make(map[string]blockchain.Callbacks)
+	f.callbacks.opHandlers = make(map[string]core.OperationCallbacks)
 
 	if fabconnectConf.GetString(ffresty.HTTPConfigURL) == "" {
 		return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "url", "blockchain.fabric.fabconnect")
@@ -269,6 +271,10 @@ func (f *Fabric) SetHandler(namespace string, handler blockchain.Callbacks) {
 	f.callbacks.handlers[namespace] = handler
 }
 
+func (f *Fabric) SetOperationHandler(namespace string, handler core.OperationCallbacks) {
+	f.callbacks.opHandlers[namespace] = handler
+}
+
 func (f *Fabric) Start() (err error) {
 	return f.wsconn.Connect()
 }
@@ -456,8 +462,8 @@ func (f *Fabric) handleReceipt(ctx context.Context, reply fftypes.JSONObject) {
 	if replyType != "TransactionSuccess" {
 		updateType = core.OpStatusFailed
 	}
-	l.Infof("Fabconnect '%s' reply tx=%s (request=%s) %s", replyType, txHash, requestID, message)
-	f.callbacks.BlockchainOpUpdate(ctx, f, requestID, updateType, txHash, message, reply)
+	l.Infof("Received operation update: status=%s request=%s tx=%s message=%s", updateType, requestID, txHash, message)
+	f.callbacks.OperationUpdate(ctx, f, requestID, updateType, txHash, message, reply)
 }
 
 func (f *Fabric) AddFireflySubscription(ctx context.Context, namespace string, location *fftypes.JSONAny, firstEvent string) (string, error) {
diff --git a/internal/broadcast/manager.go b/internal/broadcast/manager.go
index f3a440e70..46be34e9e 100644
--- a/internal/broadcast/manager.go
+++ b/internal/broadcast/manager.go
@@ -57,7 +57,7 @@ type Manager interface {
 
 type broadcastManager struct {
 	ctx                   context.Context
-	namespace             string
+	namespace             core.NamespaceRef
 	database              database.Plugin
 	identity              identity.Manager
 	data                  data.Manager
@@ -71,7 +71,7 @@ type broadcastManager struct {
 	operations            operations.Manager
 }
 
-func NewBroadcastManager(ctx context.Context, ns string, di database.Plugin, bi blockchain.Plugin, dx dataexchange.Plugin, si sharedstorage.Plugin, im identity.Manager, dm data.Manager, ba batch.Manager, sa syncasync.Bridge, mult multiparty.Manager, mm metrics.Manager, om operations.Manager) (Manager, error) {
+func NewBroadcastManager(ctx context.Context, ns core.NamespaceRef, di database.Plugin, bi blockchain.Plugin, dx dataexchange.Plugin, si sharedstorage.Plugin, im identity.Manager, dm data.Manager, ba batch.Manager, sa syncasync.Bridge, mult multiparty.Manager, mm metrics.Manager, om operations.Manager) (Manager, error) {
 	if di == nil || im == nil || dm == nil || bi == nil || dx == nil || si == nil || ba == nil || mm == nil || om == nil || mult == nil {
 		return nil, i18n.NewError(ctx, coremsgs.MsgInitializationNilDepError, "BroadcastManager")
 	}
@@ -141,7 +141,7 @@ func (bm *broadcastManager) dispatchBatch(ctx context.Context, state *batch.Disp
 	// We are in an (indefinite) retry cycle from the batch processor to dispatch this batch, that is only
 	// termianted with shutdown. So we leave the operation pending on failure, as it is still being retried.
 	// The user will still have the failure details recorded.
-	outputs, err := bm.operations.RunOperation(ctx, opUploadBatch(op, batch, &state.Persisted), operations.RemainPendingOnFailure)
+	outputs, err := bm.operations.RunOperation(ctx, opUploadBatch(op, batch), operations.RemainPendingOnFailure)
 	if err != nil {
 		return err
 	}
diff --git a/internal/broadcast/message.go b/internal/broadcast/message.go
index 9287c016c..9ab074fa4 100644
--- a/internal/broadcast/message.go
+++ b/internal/broadcast/message.go
@@ -87,7 +87,8 @@ func (s *broadcastSender) SendAndWait(ctx context.Context) error {
 func (s *broadcastSender) setDefaults() {
 	msg := s.msg.Message
 	msg.Header.ID = fftypes.NewUUID()
-	msg.Header.Namespace = s.mgr.namespace
+	msg.Header.Namespace = s.mgr.namespace.RemoteName
+	msg.LocalNamespace = s.mgr.namespace.LocalName
 	msg.State = core.MessageStateReady
 	if msg.Header.Type == "" {
 		msg.Header.Type = core.MessageTypeBroadcast
@@ -148,7 +149,7 @@ func (s *broadcastSender) sendInternal(ctx context.Context, method sendMethod) (
 	if err := s.mgr.data.WriteNewMessage(ctx, s.msg); err != nil {
 		return err
 	}
-	log.L(ctx).Infof("Sent broadcast message %s:%s sequence=%d datacount=%d", msg.Header.Namespace, msg.Header.ID, msg.Sequence, len(s.msg.AllData))
+	log.L(ctx).Infof("Sent broadcast message %s sequence=%d datacount=%d", msg.Header.ID, msg.Sequence, len(s.msg.AllData))
 
 	return err
 }
diff --git a/internal/broadcast/operations.go b/internal/broadcast/operations.go
index a98f521fc..f4b0dc04e 100644
--- a/internal/broadcast/operations.go
+++ b/internal/broadcast/operations.go
@@ -31,8 +31,7 @@ import (
 )
 
 type uploadBatchData struct {
-	BatchPersisted *core.BatchPersisted `json:"batchPersisted"`
-	Batch          *core.Batch          `json:"batch"`
+	Batch *core.Batch `json:"batch"`
 }
 
 type uploadBlobData struct {
@@ -79,7 +78,7 @@ func (bm *broadcastManager) PrepareOperation(ctx context.Context, op *core.Opera
 		if err != nil {
 			return nil, err
 		}
-		bp, err := bm.database.GetBatchByID(ctx, bm.namespace, id)
+		bp, err := bm.database.GetBatchByID(ctx, bm.namespace.LocalName, id)
 		if err != nil {
 			return nil, err
 		} else if bp == nil {
@@ -89,14 +88,14 @@ func (bm *broadcastManager) PrepareOperation(ctx context.Context, op *core.Opera
 		if err != nil {
 			return nil, err
 		}
-		return opUploadBatch(op, batch, bp), nil
+		return opUploadBatch(op, batch), nil
 
 	case core.OpTypeSharedStorageUploadBlob:
 		dataID, err := retrieveUploadBlobInputs(ctx, op)
 		if err != nil {
 			return nil, err
 		}
-		d, err := bm.database.GetDataByID(ctx, bm.namespace, dataID, false)
+		d, err := bm.database.GetDataByID(ctx, bm.namespace.LocalName, dataID, false)
 		if err != nil {
 			return nil, err
 		} else if d == nil || d.Blob == nil {
@@ -129,6 +128,7 @@ func (bm *broadcastManager) RunOperation(ctx context.Context, op *core.PreparedO
 // uploadBatch uploads the serialized batch to public storage
 func (bm *broadcastManager) uploadBatch(ctx context.Context, data uploadBatchData) (outputs fftypes.JSONObject, complete bool, err error) {
 	// Serialize the full payload, which has already been sealed for us by the BatchManager
+	data.Batch.Namespace = bm.namespace.RemoteName
 	payload, err := json.Marshal(data.Batch)
 	if err != nil {
 		return nil, false, i18n.WrapError(ctx, err, coremsgs.MsgSerializationFailed)
@@ -173,12 +173,12 @@ func (bm *broadcastManager) OnOperationUpdate(ctx context.Context, op *core.Oper
 	return nil
 }
 
-func opUploadBatch(op *core.Operation, batch *core.Batch, batchPersisted *core.BatchPersisted) *core.PreparedOperation {
+func opUploadBatch(op *core.Operation, batch *core.Batch) *core.PreparedOperation {
 	return &core.PreparedOperation{
 		ID:        op.ID,
 		Namespace: op.Namespace,
 		Type:      op.Type,
-		Data:      uploadBatchData{Batch: batch, BatchPersisted: batchPersisted},
+		Data:      uploadBatchData{Batch: batch},
 	}
 }
 
diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go
index 7fb464421..696095e0f 100644
--- a/internal/coremsgs/en_struct_descriptions.go
+++ b/internal/coremsgs/en_struct_descriptions.go
@@ -53,20 +53,21 @@ var (
 	MessageHeaderType      = ffm("MessageHeader.type", "The type of the message")
 	MessageHeaderTxType    = ffm("MessageHeader.txtype", "The type of transaction used to order/deliver this message")
 	MessageHeaderCreated   = ffm("MessageHeader.created", "The creation time of the message")
-	MessageHeaderNamespace = ffm("MessageHeader.namespace", "The namespace of the message")
+	MessageHeaderNamespace = ffm("MessageHeader.namespace", "The namespace of the message within the multiparty network")
 	MessageHeaderGroup     = ffm("MessageHeader.group", "Private messages only - the identifier hash of the privacy group. Derived from the name and member list of the group")
 	MessageHeaderTopics    = ffm("MessageHeader.topics", "A message topic associates this message with an ordered stream of data. A custom topic should be assigned - using the default topic is discouraged")
 	MessageHeaderTag       = ffm("MessageHeader.tag", "The message tag indicates the purpose of the message to the applications that process it")
 	MessageHeaderDataHash  = ffm("MessageHeader.datahash", "A single hash representing all data in the message. Derived from the array of data ids+hashes attached to this message")
 
 	// Message field descriptions
-	MessageHeader    = ffm("Message.header", "The message header contains all fields that are used to build the message hash")
-	MessageHash      = ffm("Message.hash", "The hash of the message. Derived from the header, which includes the data hash")
-	MessageBatchID   = ffm("Message.batch", "The UUID of the batch in which the message was pinned/transferred")
-	MessageState     = ffm("Message.state", "The current state of the message")
-	MessageConfirmed = ffm("Message.confirmed", "The timestamp of when the message was confirmed/rejected")
-	MessageData      = ffm("Message.data", "The list of data elements attached to the message")
-	MessagePins      = ffm("Message.pins", "For private messages, a unique pin hash:nonce is assigned for each topic")
+	MessageHeader         = ffm("Message.header", "The message header contains all fields that are used to build the message hash")
+	MessageLocalNamespace = ffm("Message.localNamespace", "The local namespace of the message")
+	MessageHash           = ffm("Message.hash", "The hash of the message. Derived from the header, which includes the data hash")
+	MessageBatchID        = ffm("Message.batch", "The UUID of the batch in which the message was pinned/transferred")
+	MessageState          = ffm("Message.state", "The current state of the message")
+	MessageConfirmed      = ffm("Message.confirmed", "The timestamp of when the message was confirmed/rejected")
+	MessageData           = ffm("Message.data", "The list of data elements attached to the message")
+	MessagePins           = ffm("Message.pins", "For private messages, a unique pin hash:nonce is assigned for each topic")
 
 	// MessageInOut field descriptions
 	MessageInOutData  = ffm("MessageInOut.data", "For input allows you to specify data in-line in the message, that will be turned into data attachments. For output when fetchdata is used on API calls, includes the in-line data payloads of all data attachments")
@@ -87,12 +88,13 @@ var (
 	MessageRefHash = ffm("MessageRef.hash", "The hash of the referenced message")
 
 	// Group field descriptions
-	GroupNamespace = ffm("Group.namespace", "The namespace of the group")
-	GroupName      = ffm("Group.name", "The optional name of the group, allowing multiple unique groups to exist with the same list of recipients")
-	GroupMembers   = ffm("Group.members", "The list of members in this privacy group")
-	GroupMessage   = ffm("Group.message", "The message used to broadcast this group privately to the members")
-	GroupHash      = ffm("Group.hash", "The identifier hash of this group. Derived from the name and group members")
-	GroupCreated   = ffm("Group.created", "The time when the group was first used to send a message in the network")
+	GroupNamespace      = ffm("Group.namespace", "The namespace of the group within the multiparty network")
+	GroupLocalNamespace = ffm("Group.localNamespace", "The local namespace of the group")
+	GroupName           = ffm("Group.name", "The optional name of the group, allowing multiple unique groups to exist with the same list of recipients")
+	GroupMembers        = ffm("Group.members", "The list of members in this privacy group")
+	GroupMessage        = ffm("Group.message", "The message used to broadcast this group privately to the members")
+	GroupHash           = ffm("Group.hash", "The identifier hash of this group. Derived from the name and group members")
+	GroupCreated        = ffm("Group.created", "The time when the group was first used to send a message in the network")
 
 	// MemberInput field descriptions
 	MemberInputIdentity = ffm("MemberInput.identity", "The DID of the group member. On input can be a UUID or org name, and will be resolved to a DID")
diff --git a/internal/data/blobstore.go b/internal/data/blobstore.go
index e4e5f6484..4d36740f5 100644
--- a/internal/data/blobstore.go
+++ b/internal/data/blobstore.go
@@ -53,7 +53,7 @@ func (bs *blobStore) uploadVerifyBlob(ctx context.Context, id *fftypes.UUID, rea
 		copyDone <- err
 	}()
 
-	payloadRef, uploadHash, uploadSize, dxErr := bs.exchange.UploadBlob(ctx, bs.dm.namespace, *id, dxReader)
+	payloadRef, uploadHash, uploadSize, dxErr := bs.exchange.UploadBlob(ctx, bs.dm.namespace.RemoteName, *id, dxReader)
 	dxReader.Close()
 	copyErr := <-copyDone
 	if dxErr != nil {
@@ -85,17 +85,13 @@ func (bs *blobStore) UploadBlob(ctx context.Context, inData *core.DataRefOrValue
 
 	data := &core.Data{
 		ID:        fftypes.NewUUID(),
-		Namespace: bs.dm.namespace,
+		Namespace: bs.dm.namespace.LocalName,
 		Created:   fftypes.Now(),
 		Validator: inData.Validator,
 		Datatype:  inData.Datatype,
 		Value:     inData.Value,
 	}
 
-	data.ID = fftypes.NewUUID()
-	data.Namespace = bs.dm.namespace
-	data.Created = fftypes.Now()
-
 	hash, blobSize, payloadRef, err := bs.uploadVerifyBlob(ctx, data.ID, mpart.Data)
 	if err != nil {
 		return nil, err
@@ -155,7 +151,7 @@ func (bs *blobStore) DownloadBlob(ctx context.Context, dataID string) (*core.Blo
 		return nil, nil, err
 	}
 
-	data, err := bs.database.GetDataByID(ctx, bs.dm.namespace, id, false)
+	data, err := bs.database.GetDataByID(ctx, bs.dm.namespace.LocalName, id, false)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/internal/data/data_manager.go b/internal/data/data_manager.go
index 398c3260b..b9ffaf5a6 100644
--- a/internal/data/data_manager.go
+++ b/internal/data/data_manager.go
@@ -56,7 +56,7 @@ type Manager interface {
 
 type dataManager struct {
 	blobStore
-	namespace         string
+	namespace         core.NamespaceRef
 	database          database.Plugin
 	validatorCache    *ccache.Cache
 	validatorCacheTTL time.Duration
@@ -93,7 +93,7 @@ const (
 	CRORequireBatchID
 )
 
-func NewDataManager(ctx context.Context, ns string, di database.Plugin, dx dataexchange.Plugin) (Manager, error) {
+func NewDataManager(ctx context.Context, ns core.NamespaceRef, di database.Plugin, dx dataexchange.Plugin) (Manager, error) {
 	if di == nil {
 		return nil, i18n.NewError(ctx, coremsgs.MsgInitializationNilDepError, "DataManager")
 	}
@@ -128,7 +128,7 @@ func NewDataManager(ctx context.Context, ns string, di database.Plugin, dx datae
 }
 
 func (dm *dataManager) CheckDatatype(ctx context.Context, datatype *core.Datatype) error {
-	_, err := newJSONValidator(ctx, dm.namespace, datatype)
+	_, err := newJSONValidator(ctx, dm.namespace.LocalName, datatype)
 	return err
 }
 
@@ -149,14 +149,14 @@ func (dm *dataManager) getValidatorForDatatype(ctx context.Context, validator co
 		return cached.Value().(Validator), nil
 	}
 
-	datatype, err := dm.database.GetDatatypeByName(ctx, dm.namespace, datatypeRef.Name, datatypeRef.Version)
+	datatype, err := dm.database.GetDatatypeByName(ctx, dm.namespace.LocalName, datatypeRef.Name, datatypeRef.Version)
 	if err != nil {
 		return nil, err
 	}
 	if datatype == nil {
 		return nil, nil
 	}
-	v, err := newJSONValidator(ctx, dm.namespace, datatype)
+	v, err := newJSONValidator(ctx, dm.namespace.LocalName, datatype)
 	if err != nil {
 		log.L(ctx).Errorf("Invalid validator stored for '%s:%s:%s': %s", validator, dm.namespace, datatypeRef, err)
 		return nil, nil
@@ -173,7 +173,7 @@ func (dm *dataManager) GetMessageWithDataCached(ctx context.Context, msgID *ffty
 	if mce := dm.queryMessageCache(ctx, msgID, options...); mce != nil {
 		return mce.msg, mce.data, true, nil
 	}
-	msg, err = dm.database.GetMessageByID(ctx, dm.namespace, msgID)
+	msg, err = dm.database.GetMessageByID(ctx, dm.namespace.LocalName, msgID)
 	if err != nil || msg == nil {
 		return nil, nil, false, err
 	}
@@ -242,6 +242,7 @@ func (dm *dataManager) queryMessageCache(ctx context.Context, id *fftypes.UUID,
 	}
 	log.L(ctx).Debugf("Cache hit for message %s", id)
 	cached.Extend(dm.messageCacheTTL)
+	mce.msg.LocalNamespace = dm.namespace.LocalName // always populate LocalNamespace on the way out of the cache
 	return mce
 }
 
@@ -319,7 +320,7 @@ func (dm *dataManager) resolveRef(ctx context.Context, dataRef *core.DataRef) (*
 		log.L(ctx).Warnf("data is nil")
 		return nil, nil
 	}
-	d, err := dm.database.GetDataByID(ctx, dm.namespace, dataRef.ID, true)
+	d, err := dm.database.GetDataByID(ctx, dm.namespace.LocalName, dataRef.ID, true)
 	if err != nil {
 		return nil, err
 	}
@@ -398,7 +399,7 @@ func (dm *dataManager) validateInputData(ctx context.Context, inData *core.DataR
 	data = &core.Data{
 		Validator: validator,
 		Datatype:  datatype,
-		Namespace: dm.namespace,
+		Namespace: dm.namespace.LocalName,
 		Value:     value,
 		Blob:      blobRef,
 	}
@@ -476,7 +477,7 @@ func (dm *dataManager) HydrateBatch(ctx context.Context, persistedBatch *core.Ba
 	batch := persistedBatch.GenInflight(make([]*core.Message, len(manifest.Messages)), make(core.DataArray, len(manifest.Data)))
 
 	for i, mr := range manifest.Messages {
-		m, err := dm.database.GetMessageByID(ctx, dm.namespace, mr.ID)
+		m, err := dm.database.GetMessageByID(ctx, dm.namespace.LocalName, mr.ID)
 		if err != nil || m == nil {
 			return nil, i18n.WrapError(ctx, err, coremsgs.MsgFailedToRetrieve, "message", mr.ID)
 		}
@@ -484,7 +485,7 @@ func (dm *dataManager) HydrateBatch(ctx context.Context, persistedBatch *core.Ba
 		batch.Payload.Messages[i] = m.BatchMessage()
 	}
 	for i, dr := range manifest.Data {
-		d, err := dm.database.GetDataByID(ctx, dm.namespace, dr.ID, true)
+		d, err := dm.database.GetDataByID(ctx, dm.namespace.LocalName, dr.ID, true)
 		if err != nil || d == nil {
 			return nil, i18n.WrapError(ctx, err, coremsgs.MsgFailedToRetrieve, "data", dr.ID)
 		}
diff --git a/internal/database/sqlcommon/group_sql.go b/internal/database/sqlcommon/group_sql.go
index 8a93589f6..24fc59605 100644
--- a/internal/database/sqlcommon/group_sql.go
+++ b/internal/database/sqlcommon/group_sql.go
@@ -33,6 +33,7 @@ var (
 	groupColumns = []string{
 		"message_id",
 		"namespace",
+		"namespace_local",
 		"name",
 		"hash",
 		"created",
@@ -104,13 +105,12 @@ func (s *SQLCommon) attemptGroupUpdate(ctx context.Context, tx *txWrapper, group
 	return s.updateTx(ctx, groupsTable, tx,
 		sq.Update(groupsTable).
 			Set("message_id", group.Message).
-			Set("namespace", group.Namespace).
 			Set("name", group.Name).
 			Set("hash", group.Hash).
 			Set("created", group.Created).
 			Where(sq.Eq{"hash": group.Hash}),
 		func() {
-			s.callbacks.HashCollectionNSEvent(database.CollectionGroups, core.ChangeEventTypeUpdated, group.Namespace, group.Hash)
+			s.callbacks.HashCollectionNSEvent(database.CollectionGroups, core.ChangeEventTypeUpdated, group.LocalNamespace, group.Hash)
 		},
 	)
 }
@@ -122,12 +122,13 @@ func (s *SQLCommon) attemptGroupInsert(ctx context.Context, tx *txWrapper, group
 			Values(
 				group.Message,
 				group.Namespace,
+				group.LocalNamespace,
 				group.Name,
 				group.Hash,
 				group.Created,
 			),
 		func() {
-			s.callbacks.HashCollectionNSEvent(database.CollectionGroups, core.ChangeEventTypeCreated, group.Namespace, group.Hash)
+			s.callbacks.HashCollectionNSEvent(database.CollectionGroups, core.ChangeEventTypeCreated, group.LocalNamespace, group.Hash)
 		},
 		requestConflictEmptyResult,
 	)
@@ -233,6 +234,7 @@ func (s *SQLCommon) groupResult(ctx context.Context, row *sql.Rows) (*core.Group
 	err := row.Scan(
 		&group.Message,
 		&group.Namespace,
+		&group.LocalNamespace,
 		&group.Name,
 		&group.Hash,
 		&group.Created,
@@ -248,7 +250,7 @@ func (s *SQLCommon) GetGroupByHash(ctx context.Context, namespace string, hash *
 	rows, _, err := s.query(ctx, groupsTable,
 		sq.Select(groupColumns...).
 			From(groupsTable).
-			Where(sq.Eq{"hash": hash, "namespace": namespace}),
+			Where(sq.Eq{"hash": hash, "namespace_local": namespace}),
 	)
 	if err != nil {
 		return nil, err
@@ -274,7 +276,8 @@ func (s *SQLCommon) GetGroupByHash(ctx context.Context, namespace string, hash *
 }
 
 func (s *SQLCommon) GetGroups(ctx context.Context, namespace string, filter database.Filter) (group []*core.Group, res *database.FilterResult, err error) {
-	query, fop, fi, err := s.filterSelect(ctx, "", sq.Select(groupColumns...).From(groupsTable), filter, groupFilterFieldMap, []interface{}{"sequence"}, sq.Eq{"namespace": namespace})
+	query, fop, fi, err := s.filterSelect(ctx, "", sq.Select(groupColumns...).From(groupsTable),
+		filter, groupFilterFieldMap, []interface{}{"sequence"}, sq.Eq{"namespace_local": namespace})
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/internal/database/sqlcommon/message_sql.go b/internal/database/sqlcommon/message_sql.go
index 5d6a1edf8..5df652e46 100644
--- a/internal/database/sqlcommon/message_sql.go
+++ b/internal/database/sqlcommon/message_sql.go
@@ -39,6 +39,7 @@ var (
 		"key",
 		"created",
 		"namespace",
+		"namespace_local",
 		"topics",
 		"tag",
 		"group_hash",
@@ -70,6 +71,7 @@ func (s *SQLCommon) attemptMessageUpdate(ctx context.Context, tx *txWrapper, mes
 			Set("key", message.Header.Key).
 			Set("created", message.Header.Created).
 			Set("namespace", message.Header.Namespace).
+			Set("namespace_local", message.LocalNamespace).
 			Set("topics", message.Header.Topics).
 			Set("tag", message.Header.Tag).
 			Set("group_hash", message.Header.Group).
@@ -85,7 +87,7 @@ func (s *SQLCommon) attemptMessageUpdate(ctx context.Context, tx *txWrapper, mes
 				"hash": message.Hash,
 			}),
 		func() {
-			s.callbacks.OrderedUUIDCollectionNSEvent(database.CollectionMessages, core.ChangeEventTypeUpdated, message.Header.Namespace, message.Header.ID, -1 /* not applicable on update */)
+			s.callbacks.OrderedUUIDCollectionNSEvent(database.CollectionMessages, core.ChangeEventTypeUpdated, message.LocalNamespace, message.Header.ID, -1 /* not applicable on update */)
 		})
 }
 
@@ -98,6 +100,7 @@ func (s *SQLCommon) setMessageInsertValues(query sq.InsertBuilder, message *core
 		message.Header.Key,
 		message.Header.Created,
 		message.Header.Namespace,
+		message.LocalNamespace,
 		message.Header.Topics,
 		message.Header.Tag,
 		message.Header.Group,
@@ -115,7 +118,7 @@ func (s *SQLCommon) attemptMessageInsert(ctx context.Context, tx *txWrapper, mes
 	message.Sequence, err = s.insertTxExt(ctx, messagesTable, tx,
 		s.setMessageInsertValues(sq.Insert(messagesTable).Columns(msgColumns...), message),
 		func() {
-			s.callbacks.OrderedUUIDCollectionNSEvent(database.CollectionMessages, core.ChangeEventTypeCreated, message.Header.Namespace, message.Header.ID, message.Sequence)
+			s.callbacks.OrderedUUIDCollectionNSEvent(database.CollectionMessages, core.ChangeEventTypeCreated, message.LocalNamespace, message.Header.ID, message.Sequence)
 		}, requestConflictEmptyResult)
 	return err
 }
@@ -224,7 +227,7 @@ func (s *SQLCommon) InsertMessages(ctx context.Context, messages []*core.Message
 		err := s.insertTxRows(ctx, messagesTable, tx, msgQuery, func() {
 			for i, message := range messages {
 				message.Sequence = sequences[i]
-				s.callbacks.OrderedUUIDCollectionNSEvent(database.CollectionMessages, core.ChangeEventTypeCreated, message.Header.Namespace, message.Header.ID, message.Sequence)
+				s.callbacks.OrderedUUIDCollectionNSEvent(database.CollectionMessages, core.ChangeEventTypeCreated, message.LocalNamespace, message.Header.ID, message.Sequence)
 			}
 		}, sequences, true /* we want the caller to be able to retry with individual upserts */)
 		if err != nil {
@@ -403,6 +406,7 @@ func (s *SQLCommon) msgResult(ctx context.Context, row *sql.Rows) (*core.Message
 		&msg.Header.Key,
 		&msg.Header.Created,
 		&msg.Header.Namespace,
+		&msg.LocalNamespace,
 		&msg.Header.Topics,
 		&msg.Header.Tag,
 		&msg.Header.Group,
@@ -429,7 +433,7 @@ func (s *SQLCommon) GetMessageByID(ctx context.Context, namespace string, id *ff
 	rows, _, err := s.query(ctx, messagesTable,
 		sq.Select(cols...).
 			From(messagesTable).
-			Where(sq.Eq{"id": id, "namespace": namespace}),
+			Where(sq.Eq{"id": id, "namespace_local": namespace}),
 	)
 	if err != nil {
 		return nil, err
@@ -488,7 +492,7 @@ func (s *SQLCommon) GetMessageIDs(ctx context.Context, namespace string, filter
 		[]interface{}{
 			&database.SortField{Field: "confirmed", Descending: true, Nulls: database.NullsFirst},
 			"created",
-		}, sq.Eq{"namespace": namespace})
+		}, sq.Eq{"namespace_local": namespace})
 	if err != nil {
 		return nil, err
 	}
@@ -549,7 +553,7 @@ func (s *SQLCommon) GetMessages(ctx context.Context, namespace string, filter da
 		[]interface{}{
 			&database.SortField{Field: "confirmed", Descending: true, Nulls: database.NullsFirst},
 			&database.SortField{Field: "created", Descending: true},
-		}, sq.Eq{"namespace": namespace})
+		}, sq.Eq{"namespace_local": namespace})
 	if err != nil {
 		return nil, nil, err
 	}
@@ -565,7 +569,7 @@ func (s *SQLCommon) GetMessagesForData(ctx context.Context, namespace string, da
 	query, fop, fi, err := s.filterSelect(
 		ctx, "m", sq.Select(cols...).From("messages_data AS md"),
 		filter, msgFilterFieldMap, []interface{}{"sequence"},
-		sq.Eq{"md.data_id": dataID, "namespace": namespace})
+		sq.Eq{"md.data_id": dataID, "namespace_local": namespace})
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/internal/definitions/handler_contracts.go b/internal/definitions/handler_contracts.go
index 49ebe8860..9e68f2940 100644
--- a/internal/definitions/handler_contracts.go
+++ b/internal/definitions/handler_contracts.go
@@ -71,16 +71,17 @@ func (dh *definitionHandler) handleFFIBroadcast(ctx context.Context, state *core
 	if valid := dh.getSystemBroadcastPayload(ctx, msg, data, &ffi); !valid {
 		return HandlerResult{Action: ActionReject}, i18n.NewError(ctx, coremsgs.MsgDefRejectedBadPayload, "contract interface", msg.Header.ID)
 	}
-	if err := ffi.Validate(ctx, true); err != nil {
-		return HandlerResult{Action: ActionReject}, i18n.NewError(ctx, coremsgs.MsgDefRejectedValidateFail, "contract interface", ffi.ID, err)
-	}
-
 	ffi.Message = msg.Header.ID
 	return dh.handleFFIDefinition(ctx, state, &ffi, tx)
 }
 
 func (dh *definitionHandler) handleFFIDefinition(ctx context.Context, state *core.BatchState, ffi *fftypes.FFI, tx *fftypes.UUID) (HandlerResult, error) {
 	l := log.L(ctx)
+	ffi.Namespace = dh.namespace
+	if err := ffi.Validate(ctx, true); err != nil {
+		return HandlerResult{Action: ActionReject}, i18n.NewError(ctx, coremsgs.MsgDefRejectedValidateFail, "contract interface", ffi.ID, err)
+	}
+
 	if retry, err := dh.persistFFI(ctx, ffi); err != nil {
 		if retry {
 			return HandlerResult{Action: ActionRetry}, err
@@ -101,16 +102,17 @@ func (dh *definitionHandler) handleContractAPIBroadcast(ctx context.Context, sta
 	if valid := dh.getSystemBroadcastPayload(ctx, msg, data, &api); !valid {
 		return HandlerResult{Action: ActionReject}, i18n.NewError(ctx, coremsgs.MsgDefRejectedBadPayload, "contract API", msg.Header.ID)
 	}
-	if err := api.Validate(ctx, true); err != nil {
-		return HandlerResult{Action: ActionReject}, i18n.NewError(ctx, coremsgs.MsgDefRejectedValidateFail, "contract API", api.ID, err)
-	}
-
 	api.Message = msg.Header.ID
 	return dh.handleContractAPIDefinition(ctx, state, "", &api, tx)
 }
 
 func (dh *definitionHandler) handleContractAPIDefinition(ctx context.Context, state *core.BatchState, httpServerURL string, api *core.ContractAPI, tx *fftypes.UUID) (HandlerResult, error) {
 	l := log.L(ctx)
+	api.Namespace = dh.namespace
+	if err := api.Validate(ctx, true); err != nil {
+		return HandlerResult{Action: ActionReject}, i18n.NewError(ctx, coremsgs.MsgDefRejectedValidateFail, "contract API", api.ID, err)
+	}
+
 	if retry, err := dh.persistContractAPI(ctx, httpServerURL, api); err != nil {
 		if retry {
 			return HandlerResult{Action: ActionRetry}, err
diff --git a/internal/definitions/handler_datatype.go b/internal/definitions/handler_datatype.go
index bb99dde0f..ed5983d4c 100644
--- a/internal/definitions/handler_datatype.go
+++ b/internal/definitions/handler_datatype.go
@@ -31,6 +31,7 @@ func (dh *definitionHandler) handleDatatypeBroadcast(ctx context.Context, state
 	if !valid {
 		return HandlerResult{Action: ActionReject}, i18n.NewError(ctx, coremsgs.MsgDefRejectedBadPayload, "datatype", msg.Header.ID)
 	}
+	dt.Namespace = dh.namespace
 	if err := dt.Validate(ctx, true); err != nil {
 		return HandlerResult{Action: ActionReject}, i18n.NewError(ctx, coremsgs.MsgDefRejectedValidateFail, "datatype", dt.ID, err)
 	}
diff --git a/internal/definitions/handler_identity_claim.go b/internal/definitions/handler_identity_claim.go
index 442f021c1..2644e73ea 100644
--- a/internal/definitions/handler_identity_claim.go
+++ b/internal/definitions/handler_identity_claim.go
@@ -135,6 +135,7 @@ func (dh *definitionHandler) confirmVerificationForClaim(ctx context.Context, st
 			if !dh.getSystemBroadcastPayload(ctx, candidate, data, &verification) {
 				return nil, nil
 			}
+			verification.Identity.Namespace = dh.namespace
 			identityMatches = verification.Identity.Equals(ctx, &identity.IdentityBase)
 			verificationID = verification.Claim.ID
 			verificationHash = verification.Claim.Hash
@@ -151,6 +152,7 @@ func (dh *definitionHandler) handleIdentityClaim(ctx context.Context, state *cor
 	l := log.L(ctx)
 
 	identity := identityClaim.Identity
+	identity.Namespace = dh.namespace
 	parent, retryable, err := dh.identity.VerifyIdentityChain(ctx, identity)
 	if err != nil {
 		if retryable {
diff --git a/internal/definitions/handler_identity_verification.go b/internal/definitions/handler_identity_verification.go
index 14b5e6549..cc59cb823 100644
--- a/internal/definitions/handler_identity_verification.go
+++ b/internal/definitions/handler_identity_verification.go
@@ -30,6 +30,7 @@ func (dh *definitionHandler) handleIdentityVerificationBroadcast(ctx context.Con
 	if !valid {
 		return HandlerResult{Action: ActionReject}, i18n.NewError(ctx, coremsgs.MsgDefRejectedBadPayload, "identity verification", verifyMsg.Header.ID)
 	}
+	verification.Identity.Namespace = dh.namespace
 	err := verification.Identity.Validate(ctx)
 	if err != nil || verification.Identity.Parent == nil || verification.Claim.ID == nil || verification.Claim.Hash == nil {
 		return HandlerResult{Action: ActionReject}, i18n.NewError(ctx, coremsgs.MsgDefRejectedValidateFail, "identity verification", verifyMsg.Header.ID, err)
diff --git a/internal/definitions/handler_tokenpool.go b/internal/definitions/handler_tokenpool.go
index 9482fa6e8..84c1231c6 100644
--- a/internal/definitions/handler_tokenpool.go
+++ b/internal/definitions/handler_tokenpool.go
@@ -42,6 +42,7 @@ func (dh *definitionHandler) handleTokenPoolDefinition(ctx context.Context, stat
 	// from the event (without downloading and parsing the msg)
 	correlator := pool.ID
 
+	pool.Namespace = dh.namespace
 	if err := pool.Validate(ctx); err != nil {
 		return HandlerResult{Action: ActionReject, CustomCorrelator: correlator}, i18n.NewError(ctx, coremsgs.MsgDefRejectedValidateFail, "token pool", pool.ID, err)
 	}
diff --git a/internal/definitions/sender.go b/internal/definitions/sender.go
index 542f26f84..5477c6f62 100644
--- a/internal/definitions/sender.go
+++ b/internal/definitions/sender.go
@@ -117,7 +117,6 @@ func (bm *definitionSender) sendDefinitionCommon(ctx context.Context, def core.D
 	message := &core.MessageInOut{
 		Message: core.Message{
 			Header: core.MessageHeader{
-				Namespace: bm.namespace,
 				Type:      core.MessageTypeDefinition,
 				Topics:    core.FFStringArray{def.Topic()},
 				Tag:       tag,
diff --git a/internal/definitions/sender_contracts.go b/internal/definitions/sender_contracts.go
index 41b4919a9..d72f04b82 100644
--- a/internal/definitions/sender_contracts.go
+++ b/internal/definitions/sender_contracts.go
@@ -25,7 +25,6 @@ import (
 
 func (bm *definitionSender) DefineFFI(ctx context.Context, ffi *fftypes.FFI, waitConfirm bool) error {
 	ffi.ID = fftypes.NewUUID()
-	ffi.Namespace = bm.namespace
 	for _, method := range ffi.Methods {
 		method.ID = fftypes.NewUUID()
 	}
@@ -34,14 +33,17 @@ 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
 		}
 
+		ffi.Namespace = ""
 		msg, err := bm.sendDefinitionDefault(ctx, ffi, core.SystemTagDefineFFI, waitConfirm)
 		if msg != nil {
 			ffi.Message = msg.Header.ID
 		}
+		ffi.Namespace = bm.namespace
 		return err
 	}
 
@@ -52,17 +54,19 @@ func (bm *definitionSender) DefineFFI(ctx context.Context, ffi *fftypes.FFI, wai
 
 func (bm *definitionSender) DefineContractAPI(ctx context.Context, httpServerURL string, api *core.ContractAPI, waitConfirm bool) error {
 	api.ID = fftypes.NewUUID()
-	api.Namespace = bm.namespace
 
 	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
 		}
 
+		api.Namespace = ""
 		msg, err := bm.sendDefinitionDefault(ctx, api, core.SystemTagDefineContractAPI, waitConfirm)
 		if msg != nil {
 			api.Message = msg.Header.ID
 		}
+		api.Namespace = bm.namespace
 		return err
 	}
 
diff --git a/internal/definitions/sender_datatype.go b/internal/definitions/sender_datatype.go
index 4d26cd397..63fbd2c80 100644
--- a/internal/definitions/sender_datatype.go
+++ b/internal/definitions/sender_datatype.go
@@ -29,7 +29,6 @@ func (bm *definitionSender) DefineDatatype(ctx context.Context, datatype *core.D
 	// Validate the input data definition data
 	datatype.ID = fftypes.NewUUID()
 	datatype.Created = fftypes.Now()
-	datatype.Namespace = bm.namespace
 	if datatype.Validator == "" {
 		datatype.Validator = core.ValidatorTypeJSON
 	}
@@ -44,10 +43,12 @@ func (bm *definitionSender) DefineDatatype(ctx context.Context, datatype *core.D
 			return err
 		}
 
+		datatype.Namespace = ""
 		msg, err := bm.sendDefinitionDefault(ctx, datatype, core.SystemTagDefineDatatype, waitConfirm)
 		if msg != nil {
 			datatype.Message = msg.Header.ID
 		}
+		datatype.Namespace = bm.namespace
 		return err
 	}
 
diff --git a/internal/definitions/sender_identity.go b/internal/definitions/sender_identity.go
index 757f07b1e..5a4abd5c5 100644
--- a/internal/definitions/sender_identity.go
+++ b/internal/definitions/sender_identity.go
@@ -25,7 +25,7 @@ import (
 
 // ClaimIdentity is a special form of CreateDefinition where the signing identity does not need to have been pre-registered
 // The blockchain "key" will be normalized, but the "author" will pass through unchecked
-func (bm *definitionSender) ClaimIdentity(ctx context.Context, def *core.IdentityClaim, signingIdentity *core.SignerRef, parentSigner *core.SignerRef, waitConfirm bool) error {
+func (bm *definitionSender) ClaimIdentity(ctx context.Context, claim *core.IdentityClaim, signingIdentity *core.SignerRef, parentSigner *core.SignerRef, waitConfirm bool) error {
 	if bm.multiparty {
 		var err error
 		signingIdentity.Key, err = bm.identity.NormalizeSigningKey(ctx, signingIdentity.Key, identity.KeyNormalizationBlockchainPlugin)
@@ -33,11 +33,12 @@ func (bm *definitionSender) ClaimIdentity(ctx context.Context, def *core.Identit
 			return err
 		}
 
-		claimMsg, err := bm.sendDefinitionCommon(ctx, def, signingIdentity, core.SystemTagIdentityClaim, waitConfirm)
+		claim.Identity.Namespace = ""
+		claimMsg, err := bm.sendDefinitionCommon(ctx, claim, signingIdentity, core.SystemTagIdentityClaim, waitConfirm)
 		if err != nil {
 			return err
 		}
-		def.Identity.Messages.Claim = claimMsg.Header.ID
+		claim.Identity.Messages.Claim = claimMsg.Header.ID
 
 		// Send the verification if one is required.
 		if parentSigner != nil {
@@ -46,19 +47,20 @@ func (bm *definitionSender) ClaimIdentity(ctx context.Context, def *core.Identit
 					ID:   claimMsg.Header.ID,
 					Hash: claimMsg.Hash,
 				},
-				Identity: def.Identity.IdentityBase,
+				Identity: claim.Identity.IdentityBase,
 			}, parentSigner, core.SystemTagIdentityVerification, false)
 			if err != nil {
 				return err
 			}
-			def.Identity.Messages.Verification = verifyMsg.Header.ID
+			claim.Identity.Messages.Verification = verifyMsg.Header.ID
 		}
 
 		return nil
 	}
 
+	claim.Identity.Namespace = bm.namespace
 	return fakeBatch(ctx, func(ctx context.Context, state *core.BatchState) (HandlerResult, error) {
-		return bm.handler.handleIdentityClaim(ctx, state, &identityMsgInfo{SignerRef: *signingIdentity}, def)
+		return bm.handler.handleIdentityClaim(ctx, state, &identityMsgInfo{SignerRef: *signingIdentity}, claim)
 	})
 }
 
diff --git a/internal/definitions/sender_tokenpool.go b/internal/definitions/sender_tokenpool.go
index 0889002a3..b25fb6b18 100644
--- a/internal/definitions/sender_tokenpool.go
+++ b/internal/definitions/sender_tokenpool.go
@@ -28,10 +28,12 @@ func (bm *definitionSender) DefineTokenPool(ctx context.Context, pool *core.Toke
 			return err
 		}
 
+		pool.Pool.Namespace = ""
 		msg, err := bm.sendDefinitionDefault(ctx, pool, core.SystemTagDefinePool, waitConfirm)
 		if msg != nil {
 			pool.Pool.Message = msg.Header.ID
 		}
+		pool.Pool.Namespace = bm.namespace
 		return err
 	}
 
diff --git a/internal/events/aggregator.go b/internal/events/aggregator.go
index 2d123bd4c..8c6d966c5 100644
--- a/internal/events/aggregator.go
+++ b/internal/events/aggregator.go
@@ -570,7 +570,7 @@ func (ag *aggregator) attemptMessageDispatch(ctx context.Context, msg *core.Mess
 	state.AddFinalize(func(ctx context.Context) error {
 		// Generate the appropriate event - one per topic (events cover a single topic)
 		for _, topic := range msg.Header.Topics {
-			event := core.NewEvent(eventType, msg.Header.Namespace, msg.Header.ID, tx, topic)
+			event := core.NewEvent(eventType, ag.namespace, msg.Header.ID, tx, topic)
 			event.Correlator = msg.Header.CID
 			if customCorrelator != nil {
 				// Definition handlers can set a custom event correlator (such as a token pool ID)
diff --git a/internal/events/batch_pin_complete.go b/internal/events/batch_pin_complete.go
index 914d54281..eb319dbea 100644
--- a/internal/events/batch_pin_complete.go
+++ b/internal/events/batch_pin_complete.go
@@ -40,15 +40,11 @@ func (em *eventManager) BatchPinComplete(batchPin *blockchain.BatchPin, signingK
 		log.L(em.ctx).Errorf("Invalid BatchPin transaction - ID is nil")
 		return nil // move on
 	}
-
-	if err := fftypes.ValidateFFNameField(em.ctx, batchPin.Namespace, "namespace"); err != nil {
-		log.L(em.ctx).Errorf("Invalid transaction ID='%s' - invalid namespace '%s': %a", batchPin.TransactionID, batchPin.Namespace, err)
-		return nil // move on
-	}
-	if batchPin.Namespace != em.namespace {
+	if batchPin.Namespace != em.namespace.RemoteName {
 		log.L(em.ctx).Debugf("Ignoring batch pin from different namespace '%s'", batchPin.Namespace)
 		return nil // move on
 	}
+	batchPin.Namespace = em.namespace.LocalName
 
 	log.L(em.ctx).Infof("-> BatchPinComplete batch=%s txn=%s signingIdentity=%s", batchPin.BatchID, batchPin.Event.ProtocolID, signingKey.Value)
 	defer func() {
diff --git a/internal/events/blockchain_event.go b/internal/events/blockchain_event.go
index aac2914ce..96c1fff90 100644
--- a/internal/events/blockchain_event.go
+++ b/internal/events/blockchain_event.go
@@ -46,7 +46,7 @@ func buildBlockchainEvent(ns string, subID *fftypes.UUID, event *blockchain.Even
 
 func (em *eventManager) getChainListenerByProtocolIDCached(ctx context.Context, protocolID string) (*core.ContractListener, error) {
 	return em.getChainListenerCached(fmt.Sprintf("pid:%s", protocolID), func() (*core.ContractListener, error) {
-		return em.database.GetContractListenerByBackendID(ctx, em.namespace, protocolID)
+		return em.database.GetContractListenerByBackendID(ctx, em.namespace.LocalName, protocolID)
 	})
 }
 
@@ -114,10 +114,11 @@ func (em *eventManager) BlockchainEvent(event *blockchain.EventWithSubscription)
 				log.L(ctx).Warnf("Event received from unknown subscription %s", event.Subscription)
 				return nil // no retry
 			}
-			if listener.Namespace != em.namespace {
+			if listener.Namespace != em.namespace.LocalName {
 				log.L(em.ctx).Debugf("Ignoring blockchain event from different namespace '%s'", listener.Namespace)
 				return nil
 			}
+			listener.Namespace = em.namespace.LocalName
 
 			chainEvent := buildBlockchainEvent(listener.Namespace, listener.ID, &event.Event, &core.BlockchainTransactionRef{
 				BlockchainID: event.BlockchainTXID,
diff --git a/internal/events/dx_callbacks.go b/internal/events/dx_callbacks.go
index d9821f1bf..ca9accfe9 100644
--- a/internal/events/dx_callbacks.go
+++ b/internal/events/dx_callbacks.go
@@ -81,6 +81,11 @@ func (em *eventManager) privateBatchReceived(peerID string, batch *core.Batch, w
 		log.L(em.ctx).Errorf("Ignoring private batch from non-multiparty network!")
 		return "", nil
 	}
+	if batch.Namespace != em.namespace.RemoteName {
+		log.L(em.ctx).Debugf("Ignoring private batch from different namespace '%s'", batch.Namespace)
+		return "", nil
+	}
+	batch.Namespace = em.namespace.LocalName
 
 	// Retry for persistence errors (not validation errors)
 	err = em.retry.Do(em.ctx, "private batch received", func(attempt int) (bool, error) {
@@ -189,12 +194,6 @@ func (em *eventManager) messageReceived(dx dataexchange.Plugin, event dataexchan
 	mr := event.MessageReceived()
 	l.Infof("Private batch received from %s peer '%s'", dx.Name(), mr.PeerID)
 
-	if mr.Transport.Batch.Namespace != em.namespace {
-		log.L(em.ctx).Debugf("Ignoring batch from different namespace '%s'", mr.Transport.Batch.Namespace)
-		event.AckWithManifest("")
-		return
-	}
-
 	manifestString, err := em.privateBatchReceived(mr.PeerID, mr.Transport.Batch, mr.Transport.Group)
 	if err != nil {
 		l.Warnf("Exited while persisting batch: %s", err)
@@ -213,7 +212,7 @@ func (em *eventManager) privateBlobReceived(dx dataexchange.Plugin, event dataex
 		event.Ack() // Still confirm the event
 		return
 	}
-	if br.Namespace != em.namespace {
+	if br.Namespace != em.namespace.RemoteName {
 		log.L(em.ctx).Debugf("Ignoring blob from different namespace '%s'", br.Namespace)
 		event.Ack() // Still confirm the event
 		return
diff --git a/internal/events/event_manager.go b/internal/events/event_manager.go
index 3f28d0602..dc8b3cd62 100644
--- a/internal/events/event_manager.go
+++ b/internal/events/event_manager.go
@@ -89,7 +89,7 @@ type EventManager interface {
 
 type eventManager struct {
 	ctx                   context.Context
-	namespace             string
+	namespace             core.NamespaceRef
 	ni                    sysmessaging.LocalNodeInfo
 	database              database.Plugin
 	txHelper              txcommon.Helper
@@ -115,7 +115,7 @@ type eventManager struct {
 	multiparty            multiparty.Manager // optional
 }
 
-func NewEventManager(ctx context.Context, ns string, ni sysmessaging.LocalNodeInfo, di database.Plugin, bi blockchain.Plugin, im identity.Manager, dh definitions.Handler, dm data.Manager, ds definitions.Sender, bm broadcast.Manager, pm privatemessaging.Manager, am assets.Manager, sd shareddownload.Manager, mm metrics.Manager, txHelper txcommon.Helper, transports map[string]events.Plugin, mp multiparty.Manager) (EventManager, error) {
+func NewEventManager(ctx context.Context, ns core.NamespaceRef, ni sysmessaging.LocalNodeInfo, di database.Plugin, bi blockchain.Plugin, im identity.Manager, dh definitions.Handler, dm data.Manager, ds definitions.Sender, bm broadcast.Manager, pm privatemessaging.Manager, am assets.Manager, sd shareddownload.Manager, mm metrics.Manager, txHelper txcommon.Helper, transports map[string]events.Plugin, mp multiparty.Manager) (EventManager, error) {
 	if ni == nil || di == nil || im == nil || dh == nil || dm == nil || ds == nil || am == nil {
 		return nil, i18n.NewError(ctx, coremsgs.MsgInitializationNilDepError, "EventManager")
 	}
@@ -151,12 +151,12 @@ func NewEventManager(ctx context.Context, ns string, ni sysmessaging.LocalNodeIn
 	ie, _ := eifactory.GetPlugin(ctx, system.SystemEventsTransport)
 	em.internalEvents = ie.(*system.Events)
 	if bi != nil {
-		em.aggregator = newAggregator(ctx, ns, di, bi, pm, dh, im, dm, newPinNotifier, mm)
+		em.aggregator = newAggregator(ctx, ns.LocalName, di, bi, pm, dh, im, dm, newPinNotifier, mm)
 		em.blobReceiver = newBlobReceiver(ctx, em.aggregator)
 	}
 
 	var err error
-	if em.subManager, err = newSubscriptionManager(ctx, ns, di, dm, newEventNotifier, bm, pm, txHelper, transports); err != nil {
+	if em.subManager, err = newSubscriptionManager(ctx, ns.LocalName, di, dm, newEventNotifier, bm, pm, txHelper, transports); err != nil {
 		return nil, err
 	}
 
@@ -235,7 +235,7 @@ func (em *eventManager) CreateUpdateDurableSubscription(ctx context.Context, sub
 	} else {
 		// We lock in the starting sequence at creation time, rather than when the first dispatcher
 		// starts, as that's a more obvious behavior for users
-		sequence, err := calcFirstOffset(ctx, em.namespace, em.database, subDef.Options.FirstEvent)
+		sequence, err := calcFirstOffset(ctx, em.namespace.LocalName, em.database, subDef.Options.FirstEvent)
 		if err != nil {
 			return err
 		}
@@ -249,7 +249,7 @@ func (em *eventManager) CreateUpdateDurableSubscription(ctx context.Context, sub
 
 func (em *eventManager) DeleteDurableSubscription(ctx context.Context, subDef *core.Subscription) (err error) {
 	// The event in the database for the deletion of the susbscription, will asynchronously update the submanager
-	return em.database.DeleteSubscriptionByID(ctx, em.namespace, subDef.ID)
+	return em.database.DeleteSubscriptionByID(ctx, em.namespace.LocalName, subDef.ID)
 }
 
 func (em *eventManager) AddSystemEventListener(ns string, el system.EventListener) error {
diff --git a/internal/events/network_action.go b/internal/events/network_action.go
index dee23aa3b..7f61e4936 100644
--- a/internal/events/network_action.go
+++ b/internal/events/network_action.go
@@ -26,7 +26,7 @@ import (
 )
 
 func (em *eventManager) actionTerminate(location *fftypes.JSONAny, event *blockchain.Event) error {
-	namespace, err := em.database.GetNamespace(em.ctx, em.namespace)
+	namespace, err := em.database.GetNamespace(em.ctx, em.namespace.LocalName)
 	if err != nil {
 		return err
 	}
diff --git a/internal/events/persist_batch.go b/internal/events/persist_batch.go
index ca6a1aa16..ccdaca474 100644
--- a/internal/events/persist_batch.go
+++ b/internal/events/persist_batch.go
@@ -99,6 +99,7 @@ func (em *eventManager) validateAndPersistBatchContent(ctx context.Context, batc
 		if valid, err = em.checkAndInitiateBlobDownloads(ctx, batch, i, data); !valid || err != nil {
 			return false, err
 		}
+		data.Namespace = em.namespace.LocalName
 		dataByID[*data.ID] = data
 	}
 
@@ -107,6 +108,7 @@ func (em *eventManager) validateAndPersistBatchContent(ctx context.Context, batc
 		if valid = em.validateBatchMessage(ctx, batch, i, msg); !valid {
 			return false, nil
 		}
+		msg.LocalNamespace = em.namespace.LocalName
 	}
 
 	// We require that the batch contains exactly the set of data that is in the messages - no more or less.
@@ -211,7 +213,7 @@ func (em *eventManager) validateBatchMessage(ctx context.Context, batch *core.Ba
 }
 
 func (em *eventManager) sentByUs(ctx context.Context, batch *core.Batch) bool {
-	localNode := em.ni.GetNodeUUID(ctx, batch.Namespace)
+	localNode := em.ni.GetNodeUUID(ctx)
 	if batch.Node == nil {
 		// This is from a node that hasn't yet completed registration, so we can't optimize
 		return false
diff --git a/internal/events/reply_sender.go b/internal/events/reply_sender.go
index 72c65cfbf..2c0d2c513 100644
--- a/internal/events/reply_sender.go
+++ b/internal/events/reply_sender.go
@@ -42,6 +42,6 @@ func (ed *eventDispatcher) sendReply(ctx context.Context, event *core.Event, rep
 	if err != nil {
 		log.L(ctx).Errorf("Failed to send reply: %s", err)
 	} else {
-		log.L(ctx).Infof("Sent reply %s:%s (%s) cid=%s to event '%s'", reply.Header.Namespace, reply.Header.ID, reply.Header.Type, reply.Header.CID, event.ID)
+		log.L(ctx).Infof("Sent reply %s (%s) cid=%s to event '%s'", reply.Header.ID, reply.Header.Type, reply.Header.CID, event.ID)
 	}
 }
diff --git a/internal/events/ss_callbacks.go b/internal/events/ss_callbacks.go
index f31e3fe96..78d3bb744 100644
--- a/internal/events/ss_callbacks.go
+++ b/internal/events/ss_callbacks.go
@@ -44,10 +44,11 @@ func (em *eventManager) SharedStorageBatchDownloaded(ss sharedstorage.Plugin, pa
 	}
 	l.Infof("Shared storage batch downloaded from %s '%s' id=%s (len=%d)", ss.Name(), payloadRef, batch.ID, len(data))
 
-	if batch.Namespace != em.namespace {
-		l.Errorf("Invalid batch '%s'. Namespace in batch '%s' does not match pin namespace '%s'", batch.ID, batch.Namespace, em.namespace)
+	if batch.Namespace != em.namespace.RemoteName {
+		log.L(em.ctx).Debugf("Ignoring shared storage batch from different namespace '%s'", batch.Namespace)
 		return nil, nil // This is not retryable. skip this batch
 	}
+	batch.Namespace = em.namespace.LocalName
 
 	err = em.retry.Do(em.ctx, "persist batch", func(attempt int) (bool, error) {
 		err := em.database.RunAsGroup(em.ctx, func(ctx context.Context) error {
diff --git a/internal/events/token_pool_created.go b/internal/events/token_pool_created.go
index ed93ce6f2..2ca1f09e0 100644
--- a/internal/events/token_pool_created.go
+++ b/internal/events/token_pool_created.go
@@ -81,7 +81,7 @@ func (em *eventManager) findTXOperation(ctx context.Context, tx *fftypes.UUID, o
 		fb.Eq("tx", tx),
 		fb.Eq("type", opType),
 	)
-	if operations, _, err := em.database.GetOperations(ctx, em.namespace, filter); err != nil {
+	if operations, _, err := em.database.GetOperations(ctx, em.namespace.LocalName, filter); err != nil {
 		return nil, err
 	} else if len(operations) > 0 {
 		return operations[0], nil
@@ -90,7 +90,7 @@ func (em *eventManager) findTXOperation(ctx context.Context, tx *fftypes.UUID, o
 }
 
 func (em *eventManager) shouldConfirm(ctx context.Context, pool *tokens.TokenPool) (existingPool *core.TokenPool, err error) {
-	if existingPool, err = em.database.GetTokenPoolByLocator(ctx, em.namespace, pool.Connector, pool.PoolLocator); err != nil || existingPool == nil {
+	if existingPool, err = em.database.GetTokenPoolByLocator(ctx, em.namespace.LocalName, pool.Connector, pool.PoolLocator); err != nil || existingPool == nil {
 		return existingPool, err
 	}
 	if err = addPoolDetailsFromPlugin(existingPool, pool); err != nil {
@@ -147,10 +147,6 @@ func (em *eventManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPoo
 				return err
 			}
 			if existingPool != nil {
-				if existingPool.Namespace != em.namespace {
-					log.L(em.ctx).Debugf("Ignoring token pool from different namespace '%s'", existingPool.Namespace)
-					return nil
-				}
 				if existingPool.State == core.TokenPoolStateConfirmed {
 					return nil // already confirmed
 				}
@@ -167,11 +163,6 @@ func (em *eventManager) TokenPoolCreated(ti tokens.Plugin, pool *tokens.TokenPoo
 			if announcePool, err = em.shouldAnnounce(ctx, pool); err != nil {
 				return err
 			} else if announcePool != nil {
-				if announcePool.Namespace != em.namespace {
-					log.L(em.ctx).Debugf("Ignoring token pool from different namespace '%s'", announcePool.Namespace)
-					announcePool = nil
-					return nil
-				}
 				return nil // trigger announce after completion of database transaction
 			}
 
diff --git a/internal/events/tokens_approved.go b/internal/events/tokens_approved.go
index 185eac364..6667263c0 100644
--- a/internal/events/tokens_approved.go
+++ b/internal/events/tokens_approved.go
@@ -43,7 +43,7 @@ func (em *eventManager) loadApprovalID(ctx context.Context, tx *fftypes.UUID, ap
 		fb.Eq("tx", tx),
 		fb.Eq("type", core.OpTypeTokenApproval),
 	)
-	operations, _, err := em.database.GetOperations(ctx, em.namespace, filter)
+	operations, _, err := em.database.GetOperations(ctx, em.namespace.LocalName, filter)
 	if err != nil {
 		return nil, err
 	}
@@ -55,7 +55,7 @@ func (em *eventManager) loadApprovalID(ctx context.Context, tx *fftypes.UUID, ap
 			log.L(ctx).Warnf("Failed to read operation inputs for token approval '%s': %s", approval.Subject, err)
 		} else if input != nil && input.Connector == approval.Connector && input.Pool.Equals(approval.Pool) {
 			// Check if the LocalID has already been used
-			if existing, err := em.database.GetTokenApprovalByID(ctx, em.namespace, input.LocalID); err != nil {
+			if existing, err := em.database.GetTokenApprovalByID(ctx, em.namespace.LocalName, input.LocalID); err != nil {
 				return nil, err
 			} else if existing == nil {
 				// Everything matches - use the LocalID that was assigned up-front when the operation was submitted
@@ -70,7 +70,7 @@ func (em *eventManager) loadApprovalID(ctx context.Context, tx *fftypes.UUID, ap
 func (em *eventManager) persistTokenApproval(ctx context.Context, approval *tokens.TokenApproval) (valid bool, err error) {
 	// Check that this is from a known pool
 	// TODO: should cache this lookup for efficiency
-	pool, err := em.database.GetTokenPoolByLocator(ctx, em.namespace, approval.Connector, approval.PoolLocator)
+	pool, err := em.database.GetTokenPoolByLocator(ctx, em.namespace.LocalName, approval.Connector, approval.PoolLocator)
 	if err != nil {
 		return false, err
 	}
@@ -78,15 +78,11 @@ func (em *eventManager) persistTokenApproval(ctx context.Context, approval *toke
 		log.L(ctx).Infof("Token approval received for unknown pool '%s' - ignoring: %s", approval.PoolLocator, approval.Event.ProtocolID)
 		return false, nil
 	}
-	if pool.Namespace != em.namespace {
-		log.L(em.ctx).Debugf("Ignoring token approval from different namespace '%s'", pool.Namespace)
-		return false, nil
-	}
 	approval.Namespace = pool.Namespace
 	approval.Pool = pool.ID
 
 	// Check that approval has not already been recorded
-	if existing, err := em.database.GetTokenApprovalByProtocolID(ctx, em.namespace, approval.Connector, approval.ProtocolID); err != nil {
+	if existing, err := em.database.GetTokenApprovalByProtocolID(ctx, em.namespace.LocalName, approval.Connector, approval.ProtocolID); err != nil {
 		return false, err
 	} else if existing != nil {
 		log.L(ctx).Warnf("Token approval '%s' has already been recorded - ignoring", approval.ProtocolID)
diff --git a/internal/events/tokens_transferred.go b/internal/events/tokens_transferred.go
index 070d05d29..198c6a392 100644
--- a/internal/events/tokens_transferred.go
+++ b/internal/events/tokens_transferred.go
@@ -43,7 +43,7 @@ func (em *eventManager) loadTransferID(ctx context.Context, tx *fftypes.UUID, tr
 		fb.Eq("tx", tx),
 		fb.Eq("type", core.OpTypeTokenTransfer),
 	)
-	operations, _, err := em.database.GetOperations(ctx, em.namespace, filter)
+	operations, _, err := em.database.GetOperations(ctx, em.namespace.LocalName, filter)
 	if err != nil {
 		return nil, err
 	}
@@ -55,7 +55,7 @@ func (em *eventManager) loadTransferID(ctx context.Context, tx *fftypes.UUID, tr
 			log.L(ctx).Warnf("Failed to read operation inputs for token transfer '%s': %s", transfer.ProtocolID, err)
 		} else if input != nil && input.Connector == transfer.Connector && input.Pool.Equals(transfer.Pool) {
 			// Check if the LocalID has already been used
-			if existing, err := em.database.GetTokenTransferByID(ctx, em.namespace, input.LocalID); err != nil {
+			if existing, err := em.database.GetTokenTransferByID(ctx, em.namespace.LocalName, input.LocalID); err != nil {
 				return nil, err
 			} else if existing == nil {
 				// Everything matches - use the LocalID that was assigned up-front when the operation was submitted
@@ -70,7 +70,7 @@ func (em *eventManager) loadTransferID(ctx context.Context, tx *fftypes.UUID, tr
 func (em *eventManager) persistTokenTransfer(ctx context.Context, transfer *tokens.TokenTransfer) (valid bool, err error) {
 	// Check that this is from a known pool
 	// TODO: should cache this lookup for efficiency
-	pool, err := em.database.GetTokenPoolByLocator(ctx, em.namespace, transfer.Connector, transfer.PoolLocator)
+	pool, err := em.database.GetTokenPoolByLocator(ctx, em.namespace.LocalName, transfer.Connector, transfer.PoolLocator)
 	if err != nil {
 		return false, err
 	}
@@ -78,15 +78,11 @@ func (em *eventManager) persistTokenTransfer(ctx context.Context, transfer *toke
 		log.L(ctx).Infof("Token transfer received for unknown pool '%s' - ignoring: %s", transfer.PoolLocator, transfer.Event.ProtocolID)
 		return false, nil
 	}
-	if pool.Namespace != em.namespace {
-		log.L(em.ctx).Debugf("Ignoring token transfer from different namespace '%s'", pool.Namespace)
-		return false, nil
-	}
 	transfer.Namespace = pool.Namespace
 	transfer.Pool = pool.ID
 
 	// Check that transfer has not already been recorded
-	if existing, err := em.database.GetTokenTransferByProtocolID(ctx, em.namespace, transfer.Connector, transfer.ProtocolID); err != nil {
+	if existing, err := em.database.GetTokenTransferByProtocolID(ctx, em.namespace.LocalName, transfer.Connector, transfer.ProtocolID); err != nil {
 		return false, err
 	} else if existing != nil {
 		log.L(ctx).Warnf("Token transfer '%s' has already been recorded - ignoring", transfer.ProtocolID)
@@ -142,7 +138,7 @@ func (em *eventManager) TokensTransferred(ti tokens.Plugin, transfer *tokens.Tok
 			}
 
 			if transfer.Message != nil {
-				msg, err := em.database.GetMessageByID(ctx, em.namespace, transfer.Message)
+				msg, err := em.database.GetMessageByID(ctx, em.namespace.LocalName, transfer.Message)
 				switch {
 				case err != nil:
 					return err
diff --git a/internal/multiparty/manager.go b/internal/multiparty/manager.go
index ccb473b2d..0d94c02b8 100644
--- a/internal/multiparty/manager.go
+++ b/internal/multiparty/manager.go
@@ -82,7 +82,7 @@ type Contract struct {
 
 type multipartyManager struct {
 	ctx            context.Context
-	namespace      string
+	namespace      core.NamespaceRef
 	database       database.Plugin
 	blockchain     blockchain.Plugin
 	operations     operations.Manager
@@ -97,7 +97,7 @@ type multipartyManager struct {
 	}
 }
 
-func NewMultipartyManager(ctx context.Context, ns string, 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.NamespaceRef, 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")
 	}
@@ -137,7 +137,7 @@ func (mm *multipartyManager) ConfigureContract(ctx context.Context, contracts *c
 		return err
 	}
 
-	subID, err := mm.blockchain.AddFireflySubscription(ctx, mm.namespace, location, firstEvent)
+	subID, err := mm.blockchain.AddFireflySubscription(ctx, mm.namespace.RemoteName, location, firstEvent)
 	if err == nil {
 		mm.activeContract.location = location
 		mm.activeContract.firstEvent = firstEvent
@@ -204,7 +204,7 @@ func (mm *multipartyManager) SubmitNetworkAction(ctx context.Context, signingKey
 
 	op := core.NewOperation(
 		mm.blockchain,
-		mm.namespace,
+		mm.namespace.LocalName,
 		txid,
 		core.OpTypeBlockchainNetworkAction)
 	addNetworkActionInputs(op, action.Type, signingKey)
diff --git a/internal/multiparty/operations.go b/internal/multiparty/operations.go
index e80bd0ecc..278f1e257 100644
--- a/internal/multiparty/operations.go
+++ b/internal/multiparty/operations.go
@@ -87,7 +87,7 @@ func (mm *multipartyManager) PrepareOperation(ctx context.Context, op *core.Oper
 		if err != nil {
 			return nil, err
 		}
-		batch, err := mm.database.GetBatchByID(ctx, mm.namespace, batchID)
+		batch, err := mm.database.GetBatchByID(ctx, mm.namespace.LocalName, batchID)
 		if err != nil {
 			return nil, err
 		} else if batch == nil {
@@ -112,7 +112,7 @@ func (mm *multipartyManager) RunOperation(ctx context.Context, op *core.Prepared
 		// Only include namespace for V1 networks
 		var namespace string
 		if mm.activeContract.networkVersion == 1 {
-			namespace = batch.Namespace
+			namespace = mm.namespace.RemoteName
 		}
 
 		return nil, false, mm.blockchain.SubmitBatchPin(ctx, op.NamespacedIDString(), batch.Key, &blockchain.BatchPin{
diff --git a/internal/namespace/manager.go b/internal/namespace/manager.go
index bfcc89bc1..49c864d3b 100644
--- a/internal/namespace/manager.go
+++ b/internal/namespace/manager.go
@@ -36,7 +36,7 @@ import (
 	"github.com/hyperledger/firefly/internal/events/system"
 	"github.com/hyperledger/firefly/internal/identity/iifactory"
 	"github.com/hyperledger/firefly/internal/metrics"
-	multipartyManager "github.com/hyperledger/firefly/internal/multiparty"
+	"github.com/hyperledger/firefly/internal/multiparty"
 	"github.com/hyperledger/firefly/internal/orchestrator"
 	"github.com/hyperledger/firefly/internal/sharedstorage/ssfactory"
 	"github.com/hyperledger/firefly/internal/spievents"
@@ -82,6 +82,7 @@ type Manager interface {
 }
 
 type namespace struct {
+	remoteName   string
 	description  string
 	orchestrator orchestrator.Orchestrator
 	config       orchestrator.Config
@@ -201,6 +202,7 @@ func (nm *namespaceManager) Init(ctx context.Context, cancelCtx context.CancelFu
 		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
 			}
@@ -216,7 +218,8 @@ func (nm *namespaceManager) Init(ctx context.Context, cancelCtx context.CancelFu
 func (nm *namespaceManager) initNamespace(name string, ns *namespace) error {
 	or := nm.utOrchestrator
 	if or == nil {
-		or = orchestrator.NewOrchestrator(name, ns.config, ns.plugins, nm.metrics)
+		names := core.NamespaceRef{LocalName: name, RemoteName: ns.remoteName}
+		or = orchestrator.NewOrchestrator(names, ns.config, ns.plugins, nm.metrics)
 	}
 	if err := or.Init(nm.ctx, nm.cancelCtx); err != nil {
 		return err
@@ -664,9 +667,13 @@ func (nm *namespaceManager) loadNamespace(ctx context.Context, name string, inde
 	if err := fftypes.ValidateFFNameField(ctx, name, fmt.Sprintf("namespaces.predefined[%d].name", index)); err != nil {
 		return nil, err
 	}
-	if name == core.LegacySystemNamespace || conf.GetString(coreconfig.NamespaceRemoteName) == core.LegacySystemNamespace {
+	remoteName := conf.GetString(coreconfig.NamespaceRemoteName)
+	if name == core.LegacySystemNamespace || remoteName == core.LegacySystemNamespace {
 		return nil, i18n.NewError(ctx, coremsgs.MsgFFSystemReservedName, core.LegacySystemNamespace)
 	}
+	if remoteName == "" {
+		remoteName = name
+	}
 
 	multipartyConf := conf.SubSection(coreconfig.NamespaceMultiparty)
 	// If any multiparty org information is configured (here or at the root), assume multiparty mode by default
@@ -688,9 +695,9 @@ func (nm *namespaceManager) loadNamespace(ctx context.Context, name string, inde
 	if orgDesc == "" {
 		orgDesc = deprecatedOrgDesc
 	}
-	multiparty := multipartyConf.Get(coreconfig.NamespaceMultipartyEnabled)
-	if multiparty == nil {
-		multiparty = orgName != "" || orgKey != ""
+	multipartyEnabled := multipartyConf.Get(coreconfig.NamespaceMultipartyEnabled)
+	if multipartyEnabled == nil {
+		multipartyEnabled = orgName != "" || orgKey != ""
 	}
 
 	// If no plugins are listed under this namespace, use all defined plugins by default
@@ -727,10 +734,10 @@ func (nm *namespaceManager) loadNamespace(ctx context.Context, name string, inde
 	}
 	var p *orchestrator.Plugins
 	var err error
-	if multiparty.(bool) {
+	if multipartyEnabled.(bool) {
 		contractsConf := multipartyConf.SubArray(coreconfig.NamespaceMultipartyContract)
 		contractConfArraySize := contractsConf.ArraySize()
-		contracts := make([]multipartyManager.Contract, contractConfArraySize)
+		contracts := make([]multiparty.Contract, contractConfArraySize)
 
 		for i := 0; i < contractConfArraySize; i++ {
 			conf := contractsConf.ArrayEntry(i)
@@ -740,7 +747,7 @@ func (nm *namespaceManager) loadNamespace(ctx context.Context, name string, inde
 				return nil, err
 			}
 			location := fftypes.JSONAnyPtrBytes(b)
-			contract := multipartyManager.Contract{
+			contract := multiparty.Contract{
 				Location:   location,
 				FirstEvent: conf.GetString(coreconfig.NamespaceMultipartyContractFirstEvent),
 			}
@@ -766,6 +773,7 @@ func (nm *namespaceManager) loadNamespace(ctx context.Context, name string, inde
 	}
 
 	return &namespace{
+		remoteName:  remoteName,
 		description: conf.GetString(coreconfig.NamespaceDescription),
 		config:      config,
 		plugins:     *p,
diff --git a/internal/orchestrator/bound_callbacks.go b/internal/orchestrator/bound_callbacks.go
index 291bdb87e..aa7f5c48c 100644
--- a/internal/orchestrator/bound_callbacks.go
+++ b/internal/orchestrator/bound_callbacks.go
@@ -34,20 +34,10 @@ type boundCallbacks struct {
 	om operations.Manager
 }
 
-func (bc *boundCallbacks) BlockchainOpUpdate(plugin blockchain.Plugin, nsOpID string, txState blockchain.TransactionStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) {
+func (bc *boundCallbacks) OperationUpdate(plugin core.Named, nsOpID string, status core.OpStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) {
 	bc.om.SubmitOperationUpdate(plugin, &operations.OperationUpdate{
 		NamespacedOpID: nsOpID,
-		Status:         txState,
-		BlockchainTXID: blockchainTXID,
-		ErrorMessage:   errorMessage,
-		Output:         opOutput,
-	})
-}
-
-func (bc *boundCallbacks) TokenOpUpdate(plugin tokens.Plugin, nsOpID string, txState core.OpStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) {
-	bc.om.SubmitOperationUpdate(plugin, &operations.OperationUpdate{
-		NamespacedOpID: nsOpID,
-		Status:         txState,
+		Status:         status,
 		BlockchainTXID: blockchainTXID,
 		ErrorMessage:   errorMessage,
 		Output:         opOutput,
diff --git a/internal/orchestrator/chart.go b/internal/orchestrator/chart.go
index b5b34df13..802672177 100644
--- a/internal/orchestrator/chart.go
+++ b/internal/orchestrator/chart.go
@@ -49,7 +49,7 @@ func (or *orchestrator) GetChartHistogram(ctx context.Context, startTime int64,
 
 	intervals := or.getHistogramIntervals(startTime, endTime, buckets)
 
-	histogram, err := or.database().GetChartHistogram(ctx, or.namespace, intervals, collection)
+	histogram, err := or.database().GetChartHistogram(ctx, or.namespace.LocalName, intervals, collection)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/orchestrator/data_query.go b/internal/orchestrator/data_query.go
index 6f144cf13..da5aab502 100644
--- a/internal/orchestrator/data_query.go
+++ b/internal/orchestrator/data_query.go
@@ -36,7 +36,7 @@ func (or *orchestrator) GetTransactionByID(ctx context.Context, id string) (*cor
 	if err != nil {
 		return nil, err
 	}
-	return or.database().GetTransactionByID(ctx, or.namespace, u)
+	return or.database().GetTransactionByID(ctx, or.namespace.LocalName, u)
 }
 
 func (or *orchestrator) GetTransactionOperations(ctx context.Context, id string) ([]*core.Operation, *database.FilterResult, error) {
@@ -48,7 +48,7 @@ func (or *orchestrator) GetTransactionOperations(ctx context.Context, id string)
 	filter := fb.And(
 		fb.Eq("tx", u),
 	)
-	return or.database().GetOperations(ctx, or.namespace, filter)
+	return or.database().GetOperations(ctx, or.namespace.LocalName, filter)
 }
 
 func (or *orchestrator) getMessageByID(ctx context.Context, id string) (*core.Message, error) {
@@ -56,7 +56,7 @@ func (or *orchestrator) getMessageByID(ctx context.Context, id string) (*core.Me
 	if err != nil {
 		return nil, err
 	}
-	msg, err := or.database().GetMessageByID(ctx, or.namespace, u)
+	msg, err := or.database().GetMessageByID(ctx, or.namespace.LocalName, u)
 	if err == nil && msg == nil {
 		return nil, i18n.NewError(ctx, coremsgs.Msg404NotFound)
 	}
@@ -93,7 +93,7 @@ func (or *orchestrator) GetBatchByID(ctx context.Context, id string) (*core.Batc
 	if err != nil {
 		return nil, err
 	}
-	return or.database().GetBatchByID(ctx, or.namespace, u)
+	return or.database().GetBatchByID(ctx, or.namespace.LocalName, u)
 }
 
 func (or *orchestrator) GetDataByID(ctx context.Context, id string) (*core.Data, error) {
@@ -101,7 +101,7 @@ func (or *orchestrator) GetDataByID(ctx context.Context, id string) (*core.Data,
 	if err != nil {
 		return nil, err
 	}
-	return or.database().GetDataByID(ctx, or.namespace, u, true)
+	return or.database().GetDataByID(ctx, or.namespace.LocalName, u, true)
 }
 
 func (or *orchestrator) GetDatatypeByID(ctx context.Context, id string) (*core.Datatype, error) {
@@ -109,14 +109,14 @@ func (or *orchestrator) GetDatatypeByID(ctx context.Context, id string) (*core.D
 	if err != nil {
 		return nil, err
 	}
-	return or.database().GetDatatypeByID(ctx, or.namespace, u)
+	return or.database().GetDatatypeByID(ctx, or.namespace.LocalName, u)
 }
 
 func (or *orchestrator) GetDatatypeByName(ctx context.Context, name, version string) (*core.Datatype, error) {
 	if err := fftypes.ValidateFFNameFieldNoUUID(ctx, name, "name"); err != nil {
 		return nil, err
 	}
-	return or.database().GetDatatypeByName(ctx, or.namespace, name, version)
+	return or.database().GetDatatypeByName(ctx, or.namespace.LocalName, name, version)
 }
 
 func (or *orchestrator) GetOperationByID(ctx context.Context, id string) (*core.Operation, error) {
@@ -124,7 +124,7 @@ func (or *orchestrator) GetOperationByID(ctx context.Context, id string) (*core.
 	if err != nil {
 		return nil, err
 	}
-	return or.database().GetOperationByID(ctx, or.namespace, u)
+	return or.database().GetOperationByID(ctx, or.namespace.LocalName, u)
 }
 
 func (or *orchestrator) GetEventByID(ctx context.Context, id string) (*core.Event, error) {
@@ -132,19 +132,19 @@ func (or *orchestrator) GetEventByID(ctx context.Context, id string) (*core.Even
 	if err != nil {
 		return nil, err
 	}
-	return or.database().GetEventByID(ctx, or.namespace, u)
+	return or.database().GetEventByID(ctx, or.namespace.LocalName, u)
 }
 
 func (or *orchestrator) GetTransactions(ctx context.Context, filter database.AndFilter) ([]*core.Transaction, *database.FilterResult, error) {
-	return or.database().GetTransactions(ctx, or.namespace, filter)
+	return or.database().GetTransactions(ctx, or.namespace.LocalName, filter)
 }
 
 func (or *orchestrator) GetMessages(ctx context.Context, filter database.AndFilter) ([]*core.Message, *database.FilterResult, error) {
-	return or.database().GetMessages(ctx, or.namespace, filter)
+	return or.database().GetMessages(ctx, or.namespace.LocalName, filter)
 }
 
 func (or *orchestrator) GetMessagesWithData(ctx context.Context, filter database.AndFilter) ([]*core.MessageInOut, *database.FilterResult, error) {
-	msgs, fr, err := or.database().GetMessages(ctx, or.namespace, filter)
+	msgs, fr, err := or.database().GetMessages(ctx, or.namespace.LocalName, filter)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -176,7 +176,7 @@ func (or *orchestrator) getMessageTransactionID(ctx context.Context, id string)
 		if msg.BatchID == nil {
 			return nil, i18n.NewError(ctx, coremsgs.MsgBatchNotSet)
 		}
-		batch, err := or.database().GetBatchByID(ctx, or.namespace, msg.BatchID)
+		batch, err := or.database().GetBatchByID(ctx, or.namespace.LocalName, msg.BatchID)
 		if err != nil {
 			return nil, err
 		}
@@ -198,7 +198,7 @@ func (or *orchestrator) GetMessageTransaction(ctx context.Context, id string) (*
 	if err != nil {
 		return nil, err
 	}
-	return or.database().GetTransactionByID(ctx, or.namespace, txID)
+	return or.database().GetTransactionByID(ctx, or.namespace.LocalName, txID)
 }
 
 func (or *orchestrator) GetMessageEvents(ctx context.Context, id string, filter database.AndFilter) ([]*core.Event, *database.FilterResult, error) {
@@ -215,15 +215,15 @@ func (or *orchestrator) GetMessageEvents(ctx context.Context, id string, filter
 	}
 	filter = filter.Condition(filter.Builder().In("reference", referencedIDs))
 	// Execute the filter
-	return or.database().GetEvents(ctx, or.namespace, filter)
+	return or.database().GetEvents(ctx, or.namespace.LocalName, filter)
 }
 
 func (or *orchestrator) GetBatches(ctx context.Context, filter database.AndFilter) ([]*core.BatchPersisted, *database.FilterResult, error) {
-	return or.database().GetBatches(ctx, or.namespace, filter)
+	return or.database().GetBatches(ctx, or.namespace.LocalName, filter)
 }
 
 func (or *orchestrator) GetData(ctx context.Context, filter database.AndFilter) (core.DataArray, *database.FilterResult, error) {
-	return or.database().GetData(ctx, or.namespace, filter)
+	return or.database().GetData(ctx, or.namespace.LocalName, filter)
 }
 
 func (or *orchestrator) GetMessagesForData(ctx context.Context, id string, filter database.AndFilter) ([]*core.Message, *database.FilterResult, error) {
@@ -231,19 +231,19 @@ func (or *orchestrator) GetMessagesForData(ctx context.Context, id string, filte
 	if err != nil {
 		return nil, nil, err
 	}
-	return or.database().GetMessagesForData(ctx, or.namespace, u, filter)
+	return or.database().GetMessagesForData(ctx, or.namespace.LocalName, u, filter)
 }
 
 func (or *orchestrator) GetDatatypes(ctx context.Context, filter database.AndFilter) ([]*core.Datatype, *database.FilterResult, error) {
-	return or.database().GetDatatypes(ctx, or.namespace, filter)
+	return or.database().GetDatatypes(ctx, or.namespace.LocalName, filter)
 }
 
 func (or *orchestrator) GetOperations(ctx context.Context, filter database.AndFilter) ([]*core.Operation, *database.FilterResult, error) {
-	return or.database().GetOperations(ctx, or.namespace, filter)
+	return or.database().GetOperations(ctx, or.namespace.LocalName, filter)
 }
 
 func (or *orchestrator) GetEvents(ctx context.Context, filter database.AndFilter) ([]*core.Event, *database.FilterResult, error) {
-	return or.database().GetEvents(ctx, or.namespace, filter)
+	return or.database().GetEvents(ctx, or.namespace.LocalName, filter)
 }
 
 func (or *orchestrator) GetBlockchainEventByID(ctx context.Context, id string) (*core.BlockchainEvent, error) {
@@ -251,11 +251,11 @@ func (or *orchestrator) GetBlockchainEventByID(ctx context.Context, id string) (
 	if err != nil {
 		return nil, err
 	}
-	return or.database().GetBlockchainEventByID(ctx, or.namespace, u)
+	return or.database().GetBlockchainEventByID(ctx, or.namespace.LocalName, u)
 }
 
 func (or *orchestrator) GetBlockchainEvents(ctx context.Context, filter database.AndFilter) ([]*core.BlockchainEvent, *database.FilterResult, error) {
-	return or.database().GetBlockchainEvents(ctx, or.namespace, filter)
+	return or.database().GetBlockchainEvents(ctx, or.namespace.LocalName, filter)
 }
 
 func (or *orchestrator) GetTransactionBlockchainEvents(ctx context.Context, id string) ([]*core.BlockchainEvent, *database.FilterResult, error) {
@@ -264,15 +264,15 @@ func (or *orchestrator) GetTransactionBlockchainEvents(ctx context.Context, id s
 		return nil, nil, err
 	}
 	fb := database.BlockchainEventQueryFactory.NewFilter(ctx)
-	return or.database().GetBlockchainEvents(ctx, or.namespace, fb.And(fb.Eq("tx.id", u)))
+	return or.database().GetBlockchainEvents(ctx, or.namespace.LocalName, fb.And(fb.Eq("tx.id", u)))
 }
 
 func (or *orchestrator) GetPins(ctx context.Context, filter database.AndFilter) ([]*core.Pin, *database.FilterResult, error) {
-	return or.database().GetPins(ctx, or.namespace, filter)
+	return or.database().GetPins(ctx, or.namespace.LocalName, filter)
 }
 
 func (or *orchestrator) GetEventsWithReferences(ctx context.Context, filter database.AndFilter) ([]*core.EnrichedEvent, *database.FilterResult, error) {
-	events, fr, err := or.database().GetEvents(ctx, or.namespace, filter)
+	events, fr, err := or.database().GetEvents(ctx, or.namespace.LocalName, filter)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go
index a557d2441..99fc0e63b 100644
--- a/internal/orchestrator/orchestrator.go
+++ b/internal/orchestrator/orchestrator.go
@@ -177,7 +177,7 @@ type orchestrator struct {
 	ctx            context.Context
 	cancelCtx      context.CancelFunc
 	started        bool
-	namespace      string
+	namespace      core.NamespaceRef
 	config         Config
 	plugins        Plugins
 	multiparty     multiparty.Manager       // only for multiparty
@@ -201,7 +201,7 @@ type orchestrator struct {
 	txHelper       txcommon.Helper
 }
 
-func NewOrchestrator(ns string, config Config, plugins Plugins, metrics metrics.Manager) Orchestrator {
+func NewOrchestrator(ns core.NamespaceRef, config Config, plugins Plugins, metrics metrics.Manager) Orchestrator {
 	or := &orchestrator{
 		namespace: ns,
 		config:    config,
@@ -212,7 +212,11 @@ func NewOrchestrator(ns string, config Config, plugins Plugins, metrics metrics.
 }
 
 func (or *orchestrator) Init(ctx context.Context, cancelCtx context.CancelFunc) (err error) {
-	or.ctx = log.WithLogField(ctx, "ns", or.namespace)
+	namespaceLog := or.namespace.LocalName
+	if or.namespace.RemoteName != or.namespace.LocalName {
+		namespaceLog += "->" + or.namespace.RemoteName
+	}
+	or.ctx = log.WithLogField(ctx, "ns", namespaceLog)
 	or.cancelCtx = cancelCtx
 	err = or.initPlugins(or.ctx)
 	if err == nil {
@@ -253,11 +257,11 @@ func (or *orchestrator) tokens() map[string]tokens.Plugin {
 func (or *orchestrator) Start() (err error) {
 	if or.config.Multiparty.Enabled {
 		var ns *core.Namespace
-		ns, err = or.database().GetNamespace(or.ctx, or.namespace)
+		ns, err = or.database().GetNamespace(or.ctx, or.namespace.LocalName)
 		if err == nil {
 			if ns == nil {
 				ns = &core.Namespace{
-					Name:    or.namespace,
+					Name:    or.namespace.LocalName,
 					Created: fftypes.Now(),
 				}
 			}
@@ -358,19 +362,20 @@ func (or *orchestrator) MultiParty() multiparty.Manager {
 }
 
 func (or *orchestrator) initPlugins(ctx context.Context) (err error) {
-	or.plugins.Database.Plugin.SetHandler(or.namespace, or)
+	or.plugins.Database.Plugin.SetHandler(or.namespace.LocalName, or)
 
 	if or.plugins.Blockchain.Plugin != nil {
-		or.plugins.Blockchain.Plugin.SetHandler(or.namespace, &or.bc)
+		or.plugins.Blockchain.Plugin.SetHandler(or.namespace.RemoteName, &or.bc)
+		or.plugins.Blockchain.Plugin.SetOperationHandler(or.namespace.LocalName, &or.bc)
 	}
 
 	if or.plugins.SharedStorage.Plugin != nil {
-		or.plugins.SharedStorage.Plugin.SetHandler(or.namespace, &or.bc)
+		or.plugins.SharedStorage.Plugin.SetHandler(or.namespace.RemoteName, &or.bc)
 	}
 
 	if or.plugins.DataExchange.Plugin != nil {
 		fb := database.IdentityQueryFactory.NewFilter(ctx)
-		nodes, _, err := or.database().GetIdentities(ctx, or.namespace, fb.And(
+		nodes, _, err := or.database().GetIdentities(ctx, or.namespace.LocalName, fb.And(
 			fb.Eq("type", core.IdentityTypeNode),
 		))
 		if err != nil {
@@ -381,13 +386,14 @@ func (or *orchestrator) initPlugins(ctx context.Context) (err error) {
 			nodeInfo[i] = node.Profile
 		}
 		or.plugins.DataExchange.Plugin.SetNodes(nodeInfo)
-		or.plugins.DataExchange.Plugin.SetHandler(or.namespace, &or.bc)
+		or.plugins.DataExchange.Plugin.SetHandler(or.namespace.RemoteName, &or.bc)
 	}
 
 	for _, token := range or.plugins.Tokens {
-		if err := token.Plugin.SetHandler(or.namespace, &or.bc); err != nil {
+		if err := token.Plugin.SetHandler(or.namespace.LocalName, &or.bc); err != nil {
 			return err
 		}
+		token.Plugin.SetOperationHandler(or.namespace.LocalName, &or.bc)
 	}
 
 	return nil
@@ -396,11 +402,11 @@ func (or *orchestrator) initPlugins(ctx context.Context) (err error) {
 func (or *orchestrator) initManagers(ctx context.Context) (err error) {
 
 	if or.txHelper == nil {
-		or.txHelper = txcommon.NewTransactionHelper(or.namespace, or.database(), or.data)
+		or.txHelper = txcommon.NewTransactionHelper(or.namespace.LocalName, or.database(), or.data)
 	}
 
 	if or.operations == nil {
-		if or.operations, err = operations.NewOperationsManager(ctx, or.namespace, or.database(), or.txHelper); err != nil {
+		if or.operations, err = operations.NewOperationsManager(ctx, or.namespace.LocalName, or.database(), or.txHelper); err != nil {
 			return err
 		}
 	}
@@ -415,17 +421,17 @@ func (or *orchestrator) initManagers(ctx context.Context) (err error) {
 	}
 
 	if or.identity == nil {
-		or.identity, err = identity.NewIdentityManager(ctx, or.namespace, or.config.DefaultKey, or.database(), or.blockchain(), or.multiparty)
+		or.identity, err = identity.NewIdentityManager(ctx, or.namespace.LocalName, or.config.DefaultKey, or.database(), or.blockchain(), or.multiparty)
 		if err != nil {
 			return err
 		}
 	}
 
-	or.syncasync = syncasync.NewSyncAsyncBridge(ctx, or.namespace, or.database(), or.data)
+	or.syncasync = syncasync.NewSyncAsyncBridge(ctx, or.namespace.LocalName, or.database(), or.data)
 
 	if or.config.Multiparty.Enabled {
 		if or.batch == nil {
-			or.batch, err = batch.NewBatchManager(ctx, or.namespace, or, or.database(), or.data, or.txHelper)
+			or.batch, err = batch.NewBatchManager(ctx, or.namespace.LocalName, or, or.database(), or.data, or.txHelper)
 			if err != nil {
 				return err
 			}
@@ -452,7 +458,7 @@ func (or *orchestrator) initManagers(ctx context.Context) (err error) {
 	}
 
 	if or.assets == nil {
-		or.assets, err = assets.NewAssetManager(ctx, or.namespace, or.database(), or.tokens(), or.identity, or.syncasync, or.broadcast, or.messaging, or.metrics, or.operations, or.txHelper)
+		or.assets, err = assets.NewAssetManager(ctx, or.namespace.LocalName, or.database(), or.tokens(), or.identity, or.syncasync, or.broadcast, or.messaging, or.metrics, or.operations, or.txHelper)
 		if err != nil {
 			return err
 		}
@@ -460,7 +466,7 @@ func (or *orchestrator) initManagers(ctx context.Context) (err error) {
 
 	if or.blockchain() != nil {
 		if or.contracts == nil {
-			or.contracts, err = contracts.NewContractManager(ctx, or.namespace, or.database(), or.blockchain(), or.identity, or.operations, or.txHelper, or.syncasync)
+			or.contracts, err = contracts.NewContractManager(ctx, or.namespace.LocalName, or.database(), or.blockchain(), or.identity, or.operations, or.txHelper, or.syncasync)
 			if err != nil {
 				return err
 			}
@@ -468,14 +474,14 @@ func (or *orchestrator) initManagers(ctx context.Context) (err error) {
 	}
 
 	if or.defsender == nil {
-		or.defsender, or.defhandler, err = definitions.NewDefinitionSender(ctx, or.namespace, or.config.Multiparty.Enabled, or.database(), or.blockchain(), or.dataexchange(), or.broadcast, or.identity, or.data, or.assets, or.contracts)
+		or.defsender, or.defhandler, err = definitions.NewDefinitionSender(ctx, or.namespace.LocalName, or.config.Multiparty.Enabled, or.database(), or.blockchain(), or.dataexchange(), or.broadcast, or.identity, or.data, or.assets, or.contracts)
 		if err != nil {
 			return err
 		}
 	}
 
 	if or.networkmap == nil {
-		or.networkmap, err = networkmap.NewNetworkMap(ctx, or.namespace, or.database(), or.dataexchange(), or.defsender, or.identity, or.syncasync, or.multiparty)
+		or.networkmap, err = networkmap.NewNetworkMap(ctx, or.namespace.LocalName, or.database(), or.dataexchange(), or.defsender, or.identity, or.syncasync, or.multiparty)
 		if err != nil {
 			return err
 		}
@@ -520,7 +526,7 @@ func (or *orchestrator) SubmitNetworkAction(ctx context.Context, action *core.Ne
 }
 
 func (or *orchestrator) Authorize(ctx context.Context, authReq *fftypes.AuthReq) error {
-	authReq.Namespace = or.namespace
+	authReq.Namespace = or.namespace.LocalName
 	if or.plugins.Auth.Plugin != nil {
 		return or.plugins.Auth.Plugin.Authorize(ctx, authReq)
 	}
diff --git a/internal/orchestrator/persistence_events.go b/internal/orchestrator/persistence_events.go
index 1b5ac3d53..5c6963ca6 100644
--- a/internal/orchestrator/persistence_events.go
+++ b/internal/orchestrator/persistence_events.go
@@ -24,7 +24,7 @@ import (
 )
 
 func (or *orchestrator) OrderedUUIDCollectionNSEvent(resType database.OrderedUUIDCollectionNS, eventType core.ChangeEventType, ns string, id *fftypes.UUID, sequence int64) {
-	if ns != or.namespace {
+	if ns != or.namespace.LocalName {
 		log.L(or.ctx).Debugf("Ignoring database event from different namespace '%s'", ns)
 		return
 	}
@@ -37,7 +37,7 @@ func (or *orchestrator) OrderedUUIDCollectionNSEvent(resType database.OrderedUUI
 }
 
 func (or *orchestrator) OrderedCollectionNSEvent(resType database.OrderedCollectionNS, eventType core.ChangeEventType, ns string, sequence int64) {
-	if ns != or.namespace {
+	if ns != or.namespace.LocalName {
 		log.L(or.ctx).Debugf("Ignoring database event from different namespace '%s'", ns)
 		return
 	}
@@ -47,7 +47,7 @@ func (or *orchestrator) OrderedCollectionNSEvent(resType database.OrderedCollect
 }
 
 func (or *orchestrator) UUIDCollectionNSEvent(resType database.UUIDCollectionNS, eventType core.ChangeEventType, ns string, id *fftypes.UUID) {
-	if ns != or.namespace {
+	if ns != or.namespace.LocalName {
 		log.L(or.ctx).Debugf("Ignoring database event from different namespace '%s'", ns)
 		return
 	}
diff --git a/internal/orchestrator/status.go b/internal/orchestrator/status.go
index f384a157f..ca5ea4720 100644
--- a/internal/orchestrator/status.go
+++ b/internal/orchestrator/status.go
@@ -73,7 +73,7 @@ func (or *orchestrator) getPlugins() core.NodeStatusPlugins {
 	}
 }
 
-func (or *orchestrator) GetNodeUUID(ctx context.Context, ns string) (node *fftypes.UUID) {
+func (or *orchestrator) GetNodeUUID(ctx context.Context) (node *fftypes.UUID) {
 	if or.node != nil {
 		return or.node
 	}
@@ -97,7 +97,7 @@ func (or *orchestrator) GetStatus(ctx context.Context) (status *core.NodeStatus,
 		log.L(ctx).Warnf("Failed to query local org for status: %s", err)
 	}
 	status = &core.NodeStatus{
-		Namespace: or.namespace,
+		Namespace: or.namespace.LocalName,
 		Node: core.NodeStatusNode{
 			Name: config.GetString(coreconfig.NodeName),
 		},
diff --git a/internal/orchestrator/subscriptions.go b/internal/orchestrator/subscriptions.go
index 2b2313ff2..a9131eae1 100644
--- a/internal/orchestrator/subscriptions.go
+++ b/internal/orchestrator/subscriptions.go
@@ -38,7 +38,7 @@ func (or *orchestrator) CreateUpdateSubscription(ctx context.Context, subDef *co
 func (or *orchestrator) createUpdateSubscription(ctx context.Context, subDef *core.Subscription, mustNew bool) (*core.Subscription, error) {
 	subDef.ID = fftypes.NewUUID()
 	subDef.Created = fftypes.Now()
-	subDef.Namespace = or.namespace
+	subDef.Namespace = or.namespace.LocalName
 	subDef.Ephemeral = false
 	if err := fftypes.ValidateFFNameFieldNoUUID(ctx, subDef.Name, "name"); err != nil {
 		return nil, err
@@ -55,7 +55,7 @@ func (or *orchestrator) DeleteSubscription(ctx context.Context, id string) error
 	if err != nil {
 		return err
 	}
-	sub, err := or.database().GetSubscriptionByID(ctx, or.namespace, u)
+	sub, err := or.database().GetSubscriptionByID(ctx, or.namespace.LocalName, u)
 	if err != nil {
 		return err
 	}
@@ -66,7 +66,7 @@ func (or *orchestrator) DeleteSubscription(ctx context.Context, id string) error
 }
 
 func (or *orchestrator) GetSubscriptions(ctx context.Context, filter database.AndFilter) ([]*core.Subscription, *database.FilterResult, error) {
-	return or.database().GetSubscriptions(ctx, or.namespace, filter)
+	return or.database().GetSubscriptions(ctx, or.namespace.LocalName, filter)
 }
 
 func (or *orchestrator) GetSubscriptionByID(ctx context.Context, id string) (*core.Subscription, error) {
@@ -74,5 +74,5 @@ func (or *orchestrator) GetSubscriptionByID(ctx context.Context, id string) (*co
 	if err != nil {
 		return nil, err
 	}
-	return or.database().GetSubscriptionByID(ctx, or.namespace, u)
+	return or.database().GetSubscriptionByID(ctx, or.namespace.LocalName, u)
 }
diff --git a/internal/orchestrator/txn_status.go b/internal/orchestrator/txn_status.go
index f26068196..3a602aff0 100644
--- a/internal/orchestrator/txn_status.go
+++ b/internal/orchestrator/txn_status.go
@@ -101,7 +101,7 @@ func (or *orchestrator) GetTransactionStatus(ctx context.Context, id string) (*c
 			updateStatus(result, core.OpStatusPending)
 		}
 		f := database.BatchQueryFactory.NewFilter(ctx)
-		switch batches, _, err := or.database().GetBatches(ctx, or.namespace, f.Eq("tx.id", id)); {
+		switch batches, _, err := or.database().GetBatches(ctx, or.namespace.LocalName, f.Eq("tx.id", id)); {
 		case err != nil:
 			return nil, err
 		case len(batches) == 0:
@@ -120,7 +120,7 @@ func (or *orchestrator) GetTransactionStatus(ctx context.Context, id string) (*c
 	case core.TransactionTypeTokenPool:
 		// Note: no assumptions about blockchain events here (may or may not contain one)
 		f := database.TokenPoolQueryFactory.NewFilter(ctx)
-		switch pools, _, err := or.database().GetTokenPools(ctx, or.namespace, f.Eq("tx.id", id)); {
+		switch pools, _, err := or.database().GetTokenPools(ctx, or.namespace.LocalName, f.Eq("tx.id", id)); {
 		case err != nil:
 			return nil, err
 		case len(pools) == 0:
@@ -150,7 +150,7 @@ func (or *orchestrator) GetTransactionStatus(ctx context.Context, id string) (*c
 			updateStatus(result, core.OpStatusPending)
 		}
 		f := database.TokenTransferQueryFactory.NewFilter(ctx)
-		switch transfers, _, err := or.database().GetTokenTransfers(ctx, or.namespace, f.Eq("tx.id", id)); {
+		switch transfers, _, err := or.database().GetTokenTransfers(ctx, or.namespace.LocalName, f.Eq("tx.id", id)); {
 		case err != nil:
 			return nil, err
 		case len(transfers) == 0:
@@ -172,7 +172,7 @@ func (or *orchestrator) GetTransactionStatus(ctx context.Context, id string) (*c
 			updateStatus(result, core.OpStatusPending)
 		}
 		f := database.TokenApprovalQueryFactory.NewFilter(ctx)
-		switch approvals, _, err := or.database().GetTokenApprovals(ctx, or.namespace, f.Eq("tx.id", id)); {
+		switch approvals, _, err := or.database().GetTokenApprovals(ctx, or.namespace.LocalName, f.Eq("tx.id", id)); {
 		case err != nil:
 			return nil, err
 		case len(approvals) == 0:
diff --git a/internal/privatemessaging/groupmanager.go b/internal/privatemessaging/groupmanager.go
index c94f10750..5815716f5 100644
--- a/internal/privatemessaging/groupmanager.go
+++ b/internal/privatemessaging/groupmanager.go
@@ -40,7 +40,7 @@ type GroupManager interface {
 }
 
 type groupManager struct {
-	namespace     string
+	namespace     core.NamespaceRef
 	database      database.Plugin
 	identity      identity.Manager
 	data          data.Manager
@@ -63,7 +63,7 @@ func (gm *groupManager) EnsureLocalGroup(ctx context.Context, group *core.Group)
 	// the group via the blockchain.
 	// So this method checks if a group exists, and if it doesn't inserts it.
 	// We do assume the other side has sent the batch init of the group (rather than generating a second one)
-	if g, err := gm.database.GetGroupByHash(ctx, gm.namespace, group.Hash); err != nil {
+	if g, err := gm.database.GetGroupByHash(ctx, gm.namespace.LocalName, group.Hash); err != nil {
 		return false, err
 	} else if g != nil {
 		// The group already exists
@@ -72,7 +72,7 @@ func (gm *groupManager) EnsureLocalGroup(ctx context.Context, group *core.Group)
 
 	err = group.Validate(ctx, true)
 	if err != nil {
-		log.L(ctx).Errorf("Attempt to insert invalid group %s:%s: %s", group.Namespace, group.Hash, err)
+		log.L(ctx).Errorf("Attempt to insert invalid group %s: %s", group.Hash, err)
 		return false, nil
 	}
 	err = gm.database.UpsertGroup(ctx, group, database.UpsertOptimizationNew /* it could have been created by another thread, but we think we're first */)
@@ -88,7 +88,7 @@ func (gm *groupManager) groupInit(ctx context.Context, signer *core.SignerRef, g
 	data := &core.Data{
 		Validator: core.ValidatorTypeSystemDefinition,
 		ID:        fftypes.NewUUID(),
-		Namespace: group.Namespace, // must go in the same ordering context as the message
+		Namespace: gm.namespace.LocalName, // must go in the same ordering context as the message
 		Created:   fftypes.Now(),
 	}
 	b, err := json.Marshal(&group)
@@ -118,10 +118,11 @@ func (gm *groupManager) groupInit(ctx context.Context, signer *core.SignerRef, g
 
 	// Create a private send message referring to the data
 	msg := &core.Message{
-		State: core.MessageStateReady,
+		State:          core.MessageStateReady,
+		LocalNamespace: gm.namespace.LocalName,
 		Header: core.MessageHeader{
 			Group:     group.Hash,
-			Namespace: group.Namespace, // Must go into the same ordering context as the message itself
+			Namespace: gm.namespace.RemoteName, // Must go into the same ordering context as the message itself
 			Type:      core.MessageTypeGroupInit,
 			SignerRef: *signer,
 			Tag:       core.SystemTagDefineGroup,
@@ -151,11 +152,11 @@ func (gm *groupManager) GetGroupByID(ctx context.Context, hash string) (*core.Gr
 	if err != nil {
 		return nil, err
 	}
-	return gm.database.GetGroupByHash(ctx, gm.namespace, h)
+	return gm.database.GetGroupByHash(ctx, gm.namespace.LocalName, h)
 }
 
 func (gm *groupManager) GetGroups(ctx context.Context, filter database.AndFilter) ([]*core.Group, *database.FilterResult, error) {
-	return gm.database.GetGroups(ctx, gm.namespace, filter)
+	return gm.database.GetGroups(ctx, gm.namespace.LocalName, filter)
 }
 
 func (gm *groupManager) getGroupNodes(ctx context.Context, groupHash *fftypes.Bytes32, allowNil bool) (*core.Group, []*core.Identity, error) {
@@ -166,7 +167,7 @@ func (gm *groupManager) getGroupNodes(ctx context.Context, groupHash *fftypes.By
 		return ghe.group, ghe.nodes, nil
 	}
 
-	group, err := gm.database.GetGroupByHash(ctx, gm.namespace, groupHash)
+	group, err := gm.database.GetGroupByHash(ctx, gm.namespace.LocalName, groupHash)
 	if err != nil || (allowNil && group == nil) {
 		return nil, nil, err
 	}
@@ -236,12 +237,12 @@ func (gm *groupManager) ResolveInitGroup(ctx context.Context, msg *core.Message)
 	}
 
 	// Get the existing group
-	group, err := gm.database.GetGroupByHash(ctx, gm.namespace, msg.Header.Group)
+	group, err := gm.database.GetGroupByHash(ctx, gm.namespace.LocalName, msg.Header.Group)
 	if err != nil {
 		return group, err
 	}
 	if group == nil {
-		log.L(ctx).Warnf("Group %s not found for first message in context. type=%s namespace=%s", msg.Header.Group, msg.Header.Type, msg.Header.Namespace)
+		log.L(ctx).Warnf("Group %s not found for first message in context. type=%s", msg.Header.Group, msg.Header.Type)
 		return nil, nil
 	}
 	return group, nil
diff --git a/internal/privatemessaging/message.go b/internal/privatemessaging/message.go
index cb33e76e3..08b5434b0 100644
--- a/internal/privatemessaging/message.go
+++ b/internal/privatemessaging/message.go
@@ -98,7 +98,8 @@ func (s *messageSender) SendAndWait(ctx context.Context) error {
 func (s *messageSender) setDefaults() {
 	msg := s.msg.Message
 	msg.Header.ID = fftypes.NewUUID()
-	msg.Header.Namespace = s.mgr.namespace
+	msg.Header.Namespace = s.mgr.namespace.RemoteName
+	msg.LocalNamespace = s.mgr.namespace.LocalName
 	msg.State = core.MessageStateReady
 	if msg.Header.Type == "" {
 		msg.Header.Type = core.MessageTypePrivate
@@ -172,7 +173,7 @@ func (s *messageSender) sendInternal(ctx context.Context, method sendMethod) err
 	if err := s.mgr.data.WriteNewMessage(ctx, s.msg); err != nil {
 		return err
 	}
-	log.L(ctx).Infof("Sent private message %s:%s sequence=%d", msg.Header.Namespace, msg.Header.ID, msg.Sequence)
+	log.L(ctx).Infof("Sent private message %s sequence=%d", msg.Header.ID, msg.Sequence)
 
 	return nil
 }
diff --git a/internal/privatemessaging/operations.go b/internal/privatemessaging/operations.go
index 12ffb99f2..2530475a2 100644
--- a/internal/privatemessaging/operations.go
+++ b/internal/privatemessaging/operations.go
@@ -103,13 +103,13 @@ func (pm *privateMessaging) PrepareOperation(ctx context.Context, op *core.Opera
 		} else if node == nil {
 			return nil, i18n.NewError(ctx, coremsgs.Msg404NotFound)
 		}
-		group, err := pm.database.GetGroupByHash(ctx, pm.namespace, groupHash)
+		group, err := pm.database.GetGroupByHash(ctx, pm.namespace.LocalName, groupHash)
 		if err != nil {
 			return nil, err
 		} else if group == nil {
 			return nil, i18n.NewError(ctx, coremsgs.Msg404NotFound)
 		}
-		bp, err := pm.database.GetBatchByID(ctx, pm.namespace, batchID)
+		bp, err := pm.database.GetBatchByID(ctx, pm.namespace.LocalName, batchID)
 		if err != nil {
 			return nil, err
 		} else if bp == nil {
diff --git a/internal/privatemessaging/privatemessaging.go b/internal/privatemessaging/privatemessaging.go
index 75a1914d7..f215ccabb 100644
--- a/internal/privatemessaging/privatemessaging.go
+++ b/internal/privatemessaging/privatemessaging.go
@@ -62,7 +62,7 @@ type privateMessaging struct {
 	groupManager
 
 	ctx                   context.Context
-	namespace             string
+	namespace             core.NamespaceRef
 	database              database.Plugin
 	identity              identity.Manager
 	exchange              dataexchange.Plugin
@@ -85,7 +85,7 @@ type blobTransferTracker struct {
 	op       *core.PreparedOperation
 }
 
-func NewPrivateMessaging(ctx context.Context, ns string, di database.Plugin, dx dataexchange.Plugin, bi blockchain.Plugin, im identity.Manager, ba batch.Manager, dm data.Manager, sa syncasync.Bridge, mult multiparty.Manager, mm metrics.Manager, om operations.Manager) (Manager, error) {
+func NewPrivateMessaging(ctx context.Context, ns core.NamespaceRef, di database.Plugin, dx dataexchange.Plugin, bi blockchain.Plugin, im identity.Manager, ba batch.Manager, dm data.Manager, sa syncasync.Bridge, mult multiparty.Manager, mm metrics.Manager, om operations.Manager) (Manager, error) {
 	if di == nil || im == nil || dx == nil || bi == nil || ba == nil || dm == nil || mm == nil || om == nil || mult == nil {
 		return nil, i18n.NewError(ctx, coremsgs.MsgInitializationNilDepError, "PrivateMessaging")
 	}
@@ -176,6 +176,7 @@ func (pm *privateMessaging) dispatchUnpinnedBatch(ctx context.Context, state *ba
 
 func (pm *privateMessaging) dispatchBatchCommon(ctx context.Context, state *batch.DispatchState) error {
 	batch := state.Persisted.GenInflight(state.Messages, state.Data)
+	batch.Namespace = pm.namespace.RemoteName
 	tw := &core.TransportWrapper{
 		Batch: batch,
 	}
@@ -275,11 +276,11 @@ func (pm *privateMessaging) sendData(ctx context.Context, tw *core.TransportWrap
 	for i, node := range nodes {
 
 		if node.Parent.Equals(localOrg.ID) {
-			l.Debugf("Skipping send of batch for local node %s:%s for group=%s node=%s (%d/%d)", batch.Namespace, batch.ID, batch.Group, node.ID, i+1, len(nodes))
+			l.Debugf("Skipping send of batch for local node %s for group=%s node=%s (%d/%d)", batch.ID, batch.Group, node.ID, i+1, len(nodes))
 			continue
 		}
 
-		l.Debugf("Sending batch %s:%s to group=%s node=%s (%d/%d)", batch.Namespace, batch.ID, batch.Group, node.ID, i+1, len(nodes))
+		l.Debugf("Sending batch %s to group=%s node=%s (%d/%d)", batch.ID, batch.Group, node.ID, i+1, len(nodes))
 
 		var blobTrackers []*blobTransferTracker
 		var sendBatchOp *core.PreparedOperation
diff --git a/internal/privatemessaging/recipients.go b/internal/privatemessaging/recipients.go
index 66aa914f1..7bb192af6 100644
--- a/internal/privatemessaging/recipients.go
+++ b/internal/privatemessaging/recipients.go
@@ -31,7 +31,7 @@ import (
 func (pm *privateMessaging) resolveRecipientList(ctx context.Context, in *core.MessageInOut) error {
 	if in.Header.Group != nil {
 		log.L(ctx).Debugf("Group '%s' specified for message", in.Header.Group)
-		group, err := pm.database.GetGroupByHash(ctx, pm.namespace, in.Header.Group)
+		group, err := pm.database.GetGroupByHash(ctx, pm.namespace.LocalName, in.Header.Group)
 		if err != nil {
 			return err
 		}
@@ -66,7 +66,7 @@ func (pm *privateMessaging) getFirstNodeForOrg(ctx context.Context, identity *co
 			fb.Eq("parent", identity.ID),
 			fb.Eq("type", core.IdentityTypeNode),
 		)
-		nodes, _, err := pm.database.GetIdentities(ctx, pm.namespace, filter)
+		nodes, _, err := pm.database.GetIdentities(ctx, pm.namespace.LocalName, filter)
 		if err != nil || len(nodes) == 0 {
 			return nil, err
 		}
@@ -161,7 +161,7 @@ func (pm *privateMessaging) resolveLocalNode(ctx context.Context, localOrg *core
 		fb.Eq("type", core.IdentityTypeNode),
 		fb.Eq("name", pm.localNodeName),
 	)
-	nodes, _, err := pm.database.GetIdentities(ctx, pm.namespace, filter)
+	nodes, _, err := pm.database.GetIdentities(ctx, pm.namespace.LocalName, filter)
 	if err != nil {
 		return nil, err
 	}
@@ -183,8 +183,9 @@ func (pm *privateMessaging) findOrGenerateGroup(ctx context.Context, in *core.Me
 	// exists. If it doesn't, we go ahead and create it. If it does - we don't return
 	// this candidate - we return the existing group.
 	newCandidate := &core.Group{
-		GroupIdentity: *gi,
-		Created:       fftypes.Now(),
+		LocalNamespace: pm.namespace.LocalName,
+		GroupIdentity:  *gi,
+		Created:        fftypes.Now(),
 	}
 	newCandidate.Seal()
 
diff --git a/internal/reference/reference.go b/internal/reference/reference.go
index 6bb44a02d..97bdc9caf 100644
--- a/internal/reference/reference.go
+++ b/internal/reference/reference.go
@@ -379,6 +379,7 @@ func GenerateObjectsReferenceMarkdown(ctx context.Context) (map[string][]byte, e
 		},
 
 		&core.Message{
+			LocalNamespace: "ns1",
 			Header: core.MessageHeader{
 				ID:     fftypes.MustParseUUID("4ea27cce-a103-4187-b318-f7b20fd87bf3"),
 				Type:   core.MessageTypePrivate,
@@ -470,8 +471,9 @@ func GenerateObjectsReferenceMarkdown(ctx context.Context) (map[string][]byte, e
 					},
 				},
 			},
-			Message: fftypes.MustParseUUID("0b9dfb76-103d-443d-92fd-b114fe07c54d"),
-			Created: fftypes.UnixTime(1652664196),
+			LocalNamespace: "ns1",
+			Message:        fftypes.MustParseUUID("0b9dfb76-103d-443d-92fd-b114fe07c54d"),
+			Created:        fftypes.UnixTime(1652664196),
 		},
 
 		&core.Batch{
diff --git a/internal/shareddownload/download_manager.go b/internal/shareddownload/download_manager.go
index 751dc0e0e..45393e15a 100644
--- a/internal/shareddownload/download_manager.go
+++ b/internal/shareddownload/download_manager.go
@@ -51,7 +51,7 @@ type Manager interface {
 type downloadManager struct {
 	ctx                        context.Context
 	cancelFunc                 func()
-	namespace                  string
+	namespace                  core.NamespaceRef
 	database                   database.Plugin
 	sharedstorage              sharedstorage.Plugin // optional
 	dataexchange               dataexchange.Plugin
@@ -79,7 +79,7 @@ type Callbacks interface {
 	SharedStorageBlobDownloaded(hash fftypes.Bytes32, size int64, payloadRef string)
 }
 
-func NewDownloadManager(ctx context.Context, ns string, di database.Plugin, ss sharedstorage.Plugin, dx dataexchange.Plugin, om operations.Manager, cb Callbacks) (Manager, error) {
+func NewDownloadManager(ctx context.Context, ns core.NamespaceRef, di database.Plugin, ss sharedstorage.Plugin, dx dataexchange.Plugin, om operations.Manager, cb Callbacks) (Manager, error) {
 	if di == nil || dx == nil || ss == nil || cb == nil {
 		return nil, i18n.NewError(ctx, coremsgs.MsgInitializationNilDepError, "DownloadManager")
 	}
@@ -172,7 +172,7 @@ func (dm *downloadManager) recoverDownloads(startupTime *fftypes.FFTime) {
 			Sort("created").
 			Skip(page * pageSize).
 			Limit(pageSize)
-		pendingOps, _, err := dm.database.GetOperations(dm.ctx, dm.namespace, filter)
+		pendingOps, _, err := dm.database.GetOperations(dm.ctx, dm.namespace.LocalName, filter)
 		if err != nil {
 			log.L(dm.ctx).Errorf("Error while recovering pending downloads (retries=%d): %s", errorAttempts, err)
 			errorAttempts++
@@ -222,13 +222,13 @@ func (dm *downloadManager) waitAndRetryDownload(work *downloadWork) {
 }
 
 func (dm *downloadManager) InitiateDownloadBatch(ctx context.Context, tx *fftypes.UUID, payloadRef string) error {
-	op := core.NewOperation(dm.sharedstorage, dm.namespace, tx, core.OpTypeSharedStorageDownloadBatch)
+	op := core.NewOperation(dm.sharedstorage, dm.namespace.LocalName, tx, core.OpTypeSharedStorageDownloadBatch)
 	addDownloadBatchInputs(op, payloadRef)
 	return dm.createAndDispatchOp(ctx, op, opDownloadBatch(op, payloadRef))
 }
 
 func (dm *downloadManager) InitiateDownloadBlob(ctx context.Context, tx *fftypes.UUID, dataID *fftypes.UUID, payloadRef string) error {
-	op := core.NewOperation(dm.sharedstorage, dm.namespace, tx, core.OpTypeSharedStorageDownloadBlob)
+	op := core.NewOperation(dm.sharedstorage, dm.namespace.LocalName, tx, core.OpTypeSharedStorageDownloadBlob)
 	addDownloadBlobInputs(op, dataID, payloadRef)
 	return dm.createAndDispatchOp(ctx, op, opDownloadBlob(op, dataID, payloadRef))
 }
diff --git a/internal/shareddownload/operations.go b/internal/shareddownload/operations.go
index 8bf3c4654..f48463e6a 100644
--- a/internal/shareddownload/operations.go
+++ b/internal/shareddownload/operations.go
@@ -149,7 +149,7 @@ func (dm *downloadManager) downloadBlob(ctx context.Context, data downloadBlobDa
 	defer reader.Close()
 
 	// ... to data exchange
-	dxPayloadRef, hash, blobSize, err := dm.dataexchange.UploadBlob(ctx, dm.namespace, *data.DataID, reader)
+	dxPayloadRef, hash, blobSize, err := dm.dataexchange.UploadBlob(ctx, dm.namespace.RemoteName, *data.DataID, reader)
 	if err != nil {
 		return nil, false, i18n.WrapError(ctx, err, coremsgs.MsgDownloadSharedFailed, data.PayloadRef)
 	}
diff --git a/internal/sysmessaging/localnodeinfo.go b/internal/sysmessaging/localnodeinfo.go
index 586e31829..805dab9ca 100644
--- a/internal/sysmessaging/localnodeinfo.go
+++ b/internal/sysmessaging/localnodeinfo.go
@@ -25,5 +25,5 @@ import (
 // LocalNodeInfo provides an interface to query the local node info
 type LocalNodeInfo interface {
 	// GetNodeUUID returns the local node UUID in a namespace, or nil if the node is not yet registered. It is cached for fast access
-	GetNodeUUID(ctx context.Context, ns string) *fftypes.UUID
+	GetNodeUUID(ctx context.Context) *fftypes.UUID
 }
diff --git a/internal/tokens/fftokens/fftokens.go b/internal/tokens/fftokens/fftokens.go
index ee389a726..847d705fe 100644
--- a/internal/tokens/fftokens/fftokens.go
+++ b/internal/tokens/fftokens/fftokens.go
@@ -43,13 +43,14 @@ type FFTokens struct {
 }
 
 type callbacks struct {
-	handlers map[string]tokens.Callbacks
+	handlers   map[string]tokens.Callbacks
+	opHandlers map[string]core.OperationCallbacks
 }
 
-func (cb *callbacks) TokenOpUpdate(ctx context.Context, plugin tokens.Plugin, nsOpID string, txState core.OpStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) {
+func (cb *callbacks) OperationUpdate(ctx context.Context, plugin tokens.Plugin, nsOpID string, status core.OpStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject) {
 	namespace, _, _ := core.ParseNamespacedOpID(ctx, nsOpID)
-	if handler, ok := cb.handlers[namespace]; ok {
-		handler.TokenOpUpdate(plugin, nsOpID, txState, blockchainTXID, errorMessage, opOutput)
+	if handler, ok := cb.opHandlers[namespace]; ok {
+		handler.OperationUpdate(plugin, nsOpID, status, blockchainTXID, errorMessage, opOutput)
 	} else {
 		log.L(ctx).Errorf("No handler found for token operation '%s'", nsOpID)
 	}
@@ -211,6 +212,7 @@ func (ft *FFTokens) Init(ctx context.Context, name string, config config.Section
 	ft.configuredName = name
 	ft.capabilities = &tokens.Capabilities{}
 	ft.callbacks.handlers = make(map[string]tokens.Callbacks)
+	ft.callbacks.opHandlers = make(map[string]core.OperationCallbacks)
 
 	if config.GetString(ffresty.HTTPConfigURL) == "" {
 		return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "url", "tokens.fftokens")
@@ -246,6 +248,10 @@ func (ft *FFTokens) SetHandler(namespace string, handler tokens.Callbacks) error
 	return nil
 }
 
+func (ft *FFTokens) SetOperationHandler(namespace string, handler core.OperationCallbacks) {
+	ft.callbacks.opHandlers[namespace] = handler
+}
+
 func (ft *FFTokens) Start() error {
 	return ft.wsconn.Connect()
 }
@@ -265,12 +271,12 @@ func (ft *FFTokens) handleReceipt(ctx context.Context, data fftypes.JSONObject)
 		l.Errorf("Reply cannot be processed - missing fields: %+v", data)
 		return
 	}
-	replyType := core.OpStatusSucceeded
+	updateType := core.OpStatusSucceeded
 	if !success {
-		replyType = core.OpStatusFailed
+		updateType = core.OpStatusFailed
 	}
-	l.Infof("Tokens '%s' reply: request=%s message=%s", replyType, requestID, message)
-	ft.callbacks.TokenOpUpdate(ctx, ft, requestID, replyType, transactionHash, message, data)
+	l.Infof("Received operation update: status=%s request=%s message=%s", updateType, requestID, message)
+	ft.callbacks.OperationUpdate(ctx, ft, requestID, updateType, transactionHash, message, data)
 }
 
 func (ft *FFTokens) handleTokenPoolCreate(ctx context.Context, data fftypes.JSONObject) (err error) {
diff --git a/mocks/blockchainmocks/callbacks.go b/mocks/blockchainmocks/callbacks.go
index 75d48a403..12f009959 100644
--- a/mocks/blockchainmocks/callbacks.go
+++ b/mocks/blockchainmocks/callbacks.go
@@ -57,8 +57,3 @@ func (_m *Callbacks) BlockchainNetworkAction(action string, location *fftypes.JS
 
 	return r0
 }
-
-// BlockchainOpUpdate provides a mock function with given fields: plugin, nsOpID, txState, blockchainTXID, errorMessage, opOutput
-func (_m *Callbacks) BlockchainOpUpdate(plugin blockchain.Plugin, nsOpID string, txState core.OpStatus, blockchainTXID string, errorMessage string, opOutput fftypes.JSONObject) {
-	_m.Called(plugin, nsOpID, txState, blockchainTXID, errorMessage, opOutput)
-}
diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go
index f2ee50020..51fda875f 100644
--- a/mocks/blockchainmocks/plugin.go
+++ b/mocks/blockchainmocks/plugin.go
@@ -331,6 +331,11 @@ func (_m *Plugin) SetHandler(namespace string, handler blockchain.Callbacks) {
 	_m.Called(namespace, handler)
 }
 
+// SetOperationHandler provides a mock function with given fields: namespace, handler
+func (_m *Plugin) SetOperationHandler(namespace string, handler core.OperationCallbacks) {
+	_m.Called(namespace, handler)
+}
+
 // Start provides a mock function with given fields:
 func (_m *Plugin) Start() error {
 	ret := _m.Called()
diff --git a/mocks/coremocks/operation_callbacks.go b/mocks/coremocks/operation_callbacks.go
new file mode 100644
index 000000000..8116d9cb7
--- /dev/null
+++ b/mocks/coremocks/operation_callbacks.go
@@ -0,0 +1,20 @@
+// Code generated by mockery v1.0.0. DO NOT EDIT.
+
+package coremocks
+
+import (
+	fftypes "github.com/hyperledger/firefly-common/pkg/fftypes"
+	core "github.com/hyperledger/firefly/pkg/core"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// OperationCallbacks is an autogenerated mock type for the OperationCallbacks type
+type OperationCallbacks struct {
+	mock.Mock
+}
+
+// OperationUpdate provides a mock function with given fields: plugin, nsOpID, status, blockchainTXID, errorMessage, opOutput
+func (_m *OperationCallbacks) OperationUpdate(plugin core.Named, nsOpID string, status core.OpStatus, blockchainTXID string, errorMessage string, opOutput fftypes.JSONObject) {
+	_m.Called(plugin, nsOpID, status, blockchainTXID, errorMessage, opOutput)
+}
diff --git a/mocks/sysmessagingmocks/local_node_info.go b/mocks/sysmessagingmocks/local_node_info.go
index 0d6b0175e..b9c9d758e 100644
--- a/mocks/sysmessagingmocks/local_node_info.go
+++ b/mocks/sysmessagingmocks/local_node_info.go
@@ -14,13 +14,13 @@ type LocalNodeInfo struct {
 	mock.Mock
 }
 
-// GetNodeUUID provides a mock function with given fields: ctx, ns
-func (_m *LocalNodeInfo) GetNodeUUID(ctx context.Context, ns string) *fftypes.UUID {
-	ret := _m.Called(ctx, ns)
+// GetNodeUUID provides a mock function with given fields: ctx
+func (_m *LocalNodeInfo) GetNodeUUID(ctx context.Context) *fftypes.UUID {
+	ret := _m.Called(ctx)
 
 	var r0 *fftypes.UUID
-	if rf, ok := ret.Get(0).(func(context.Context, string) *fftypes.UUID); ok {
-		r0 = rf(ctx, ns)
+	if rf, ok := ret.Get(0).(func(context.Context) *fftypes.UUID); ok {
+		r0 = rf(ctx)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(*fftypes.UUID)
diff --git a/mocks/tokenmocks/callbacks.go b/mocks/tokenmocks/callbacks.go
index fe61b2d3b..2d168d465 100644
--- a/mocks/tokenmocks/callbacks.go
+++ b/mocks/tokenmocks/callbacks.go
@@ -3,12 +3,8 @@
 package tokenmocks
 
 import (
-	fftypes "github.com/hyperledger/firefly-common/pkg/fftypes"
-	core "github.com/hyperledger/firefly/pkg/core"
-
-	mock "github.com/stretchr/testify/mock"
-
 	tokens "github.com/hyperledger/firefly/pkg/tokens"
+	mock "github.com/stretchr/testify/mock"
 )
 
 // Callbacks is an autogenerated mock type for the Callbacks type
@@ -16,11 +12,6 @@ type Callbacks struct {
 	mock.Mock
 }
 
-// TokenOpUpdate provides a mock function with given fields: plugin, nsOpID, txState, blockchainTXID, errorMessage, opOutput
-func (_m *Callbacks) TokenOpUpdate(plugin tokens.Plugin, nsOpID string, txState core.OpStatus, blockchainTXID string, errorMessage string, opOutput fftypes.JSONObject) {
-	_m.Called(plugin, nsOpID, txState, blockchainTXID, errorMessage, opOutput)
-}
-
 // TokenPoolCreated provides a mock function with given fields: plugin, pool
 func (_m *Callbacks) TokenPoolCreated(plugin tokens.Plugin, pool *tokens.TokenPool) error {
 	ret := _m.Called(plugin, pool)
diff --git a/mocks/tokenmocks/plugin.go b/mocks/tokenmocks/plugin.go
index a0049ea1a..5ce8645c4 100644
--- a/mocks/tokenmocks/plugin.go
+++ b/mocks/tokenmocks/plugin.go
@@ -152,6 +152,11 @@ func (_m *Plugin) SetHandler(namespace string, handler tokens.Callbacks) error {
 	return r0
 }
 
+// SetOperationHandler provides a mock function with given fields: namespace, handler
+func (_m *Plugin) SetOperationHandler(namespace string, handler core.OperationCallbacks) {
+	_m.Called(namespace, handler)
+}
+
 // Start provides a mock function with given fields:
 func (_m *Plugin) Start() error {
 	ret := _m.Called()
diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go
index bae6c8a41..d20291243 100644
--- a/pkg/blockchain/plugin.go
+++ b/pkg/blockchain/plugin.go
@@ -39,6 +39,10 @@ type Plugin interface {
 	// If namespace is set, plugin will attempt to deliver only events for that namespace
 	SetHandler(namespace string, handler Callbacks)
 
+	// SetOperationHandler registers a handler to receive async operation status
+	// If namespace is set, plugin will attempt to deliver only events for that namespace
+	SetOperationHandler(namespace string, handler core.OperationCallbacks)
+
 	// Blockchain interface must not deliver any events until start is called
 	Start() error
 
@@ -104,13 +108,6 @@ const FireFlyActionPrefix = "firefly:"
 // has completed. However, it does not matter if these events are workload balance between the firefly core
 // cluster instances of the node.
 type Callbacks interface {
-	// BlockchainOpUpdate notifies firefly of an update to this plugin's operation within a transaction.
-	// Only success/failure and errorMessage (for errors) are modeled.
-	// opOutput can be used to add opaque protocol specific JSON from the plugin (protocol transaction ID etc.)
-	// Note this is an optional hook information, and stored separately to the confirmation of the actual event that was being submitted/sequenced.
-	// Only the party submitting the transaction will see this data.
-	BlockchainOpUpdate(plugin Plugin, nsOpID string, txState TransactionStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject)
-
 	// BatchPinComplete notifies on the arrival of a sequenced batch of messages, which might have been
 	// submitted by us, or by any other authorized party in the network.
 	//
@@ -131,14 +128,10 @@ type Callbacks interface {
 type Capabilities struct {
 }
 
-// TransactionStatus is the only architecturally significant thing that Firefly tracks on blockchain transactions.
-// All other data is consider protocol specific, and hence stored as opaque data.
-type TransactionStatus = core.OpStatus
-
 // BatchPin is the set of data pinned to the blockchain for a batch - whether it's private or broadcast.
 type BatchPin struct {
 
-	// Namespace goes in the clear on the chain
+	// Namespace goes in the clear on the chain (for network rules V1 only)
 	Namespace string
 
 	// TransactionID is the firefly transaction ID allocated before transaction submission for correlation with events (it's a UUID so no leakage)
diff --git a/pkg/core/data.go b/pkg/core/data.go
index f4eeef039..44726eb8b 100644
--- a/pkg/core/data.go
+++ b/pkg/core/data.go
@@ -79,13 +79,11 @@ func (d *Data) BatchData(batchType BatchType) *Data {
 	return &Data{
 		ID:        d.ID,
 		Validator: d.Validator,
-		Namespace: d.Namespace,
 		Hash:      d.Hash,
 		Created:   d.Created,
 		Datatype:  d.Datatype,
 		Value:     d.Value,
 		Blob:      d.Blob.BatchBlobRef(batchType),
-
 		ValueSize: d.ValueSize,
 	}
 }
diff --git a/pkg/core/datatype.go b/pkg/core/datatype.go
index a66e86973..cd21ba4f6 100644
--- a/pkg/core/datatype.go
+++ b/pkg/core/datatype.go
@@ -51,9 +51,6 @@ func (dt *Datatype) Validate(ctx context.Context, existing bool) (err error) {
 	if dt.Validator != ValidatorTypeJSON {
 		return i18n.NewError(ctx, i18n.MsgUnknownFieldValue, "validator", dt.Validator)
 	}
-	if err = fftypes.ValidateFFNameField(ctx, dt.Namespace, "namespace"); err != nil {
-		return err
-	}
 	if err = fftypes.ValidateFFNameFieldNoUUID(ctx, dt.Name, "name"); err != nil {
 		return err
 	}
diff --git a/pkg/core/group.go b/pkg/core/group.go
index 9e30ab8d7..76f88362a 100644
--- a/pkg/core/group.go
+++ b/pkg/core/group.go
@@ -35,9 +35,10 @@ type GroupIdentity struct {
 
 type Group struct {
 	GroupIdentity
-	Message *fftypes.UUID    `ffstruct:"Group" json:"message,omitempty"`
-	Hash    *fftypes.Bytes32 `ffstruct:"Group" json:"hash,omitempty"`
-	Created *fftypes.FFTime  `ffstruct:"Group" json:"created,omitempty"`
+	LocalNamespace string           `ffstruct:"Group" json:"localNamespace,omitempty"`
+	Message        *fftypes.UUID    `ffstruct:"Group" json:"message,omitempty"`
+	Hash           *fftypes.Bytes32 `ffstruct:"Group" json:"hash,omitempty"`
+	Created        *fftypes.FFTime  `ffstruct:"Group" json:"created,omitempty"`
 }
 
 type Members []*Member
diff --git a/pkg/core/identity.go b/pkg/core/identity.go
index 7dba1171a..ff829fa7b 100644
--- a/pkg/core/identity.go
+++ b/pkg/core/identity.go
@@ -166,9 +166,6 @@ func (i *IdentityBase) Validate(ctx context.Context) (err error) {
 	if i.ID == nil {
 		return i18n.NewError(ctx, i18n.MsgNilID)
 	}
-	if err = fftypes.ValidateFFNameFieldNoUUID(ctx, i.Namespace, "namespace"); err != nil {
-		return err
-	}
 	if err = fftypes.ValidateFFNameFieldNoUUID(ctx, i.Name, "name"); err != nil {
 		return err
 	}
diff --git a/pkg/core/message.go b/pkg/core/message.go
index 763937e6e..cbe4155fe 100644
--- a/pkg/core/message.go
+++ b/pkg/core/message.go
@@ -86,14 +86,15 @@ type MessageHeader struct {
 // Data is passed by reference in these messages, and a chain of hashes covering the data and the
 // details of the message, provides a verification against tampering.
 type Message struct {
-	Header    MessageHeader    `ffstruct:"Message" json:"header"`
-	Hash      *fftypes.Bytes32 `ffstruct:"Message" json:"hash,omitempty" ffexcludeinput:"true"`
-	BatchID   *fftypes.UUID    `ffstruct:"Message" json:"batch,omitempty" ffexcludeinput:"true"`
-	State     MessageState     `ffstruct:"Message" json:"state,omitempty" ffenum:"messagestate" ffexcludeinput:"true"`
-	Confirmed *fftypes.FFTime  `ffstruct:"Message" json:"confirmed,omitempty" ffexcludeinput:"true"`
-	Data      DataRefs         `ffstruct:"Message" json:"data" ffexcludeinput:"true"`
-	Pins      FFStringArray    `ffstruct:"Message" json:"pins,omitempty" ffexcludeinput:"true"`
-	Sequence  int64            `ffstruct:"Message" json:"-"` // Local database sequence used internally for batch assembly
+	Header         MessageHeader    `ffstruct:"Message" json:"header"`
+	LocalNamespace string           `ffstruct:"Message" json:"localNamespace,omitempty" ffexcludeinput:"true"`
+	Hash           *fftypes.Bytes32 `ffstruct:"Message" json:"hash,omitempty" ffexcludeinput:"true"`
+	BatchID        *fftypes.UUID    `ffstruct:"Message" json:"batch,omitempty" ffexcludeinput:"true"`
+	State          MessageState     `ffstruct:"Message" json:"state,omitempty" ffenum:"messagestate" ffexcludeinput:"true"`
+	Confirmed      *fftypes.FFTime  `ffstruct:"Message" json:"confirmed,omitempty" ffexcludeinput:"true"`
+	Data           DataRefs         `ffstruct:"Message" json:"data" ffexcludeinput:"true"`
+	Pins           FFStringArray    `ffstruct:"Message" json:"pins,omitempty" ffexcludeinput:"true"`
+	Sequence       int64            `ffstruct:"Message" json:"-"` // Local database sequence used internally for batch assembly
 }
 
 // BatchMessage is the fields in a message record that are assured to be consistent on all parties.
diff --git a/pkg/core/namespace.go b/pkg/core/namespace.go
index 851add13d..a7098d08b 100644
--- a/pkg/core/namespace.go
+++ b/pkg/core/namespace.go
@@ -62,6 +62,11 @@ type FireFlyContractInfo struct {
 	Subscription string           `ffstruct:"FireFlyContractInfo" json:"subscription,omitempty"`
 }
 
+type NamespaceRef struct {
+	LocalName  string
+	RemoteName string
+}
+
 // NetworkActionType is a type of action to perform
 type NetworkActionType = fftypes.FFEnum
 
diff --git a/pkg/core/operation.go b/pkg/core/operation.go
index 5bf25c357..cde2e4a33 100644
--- a/pkg/core/operation.go
+++ b/pkg/core/operation.go
@@ -141,3 +141,12 @@ func ParseNamespacedOpID(ctx context.Context, nsIDStr string) (string, *fftypes.
 	u, err := fftypes.ParseUUID(ctx, uuidStr)
 	return ns, u, err
 }
+
+type OperationCallbacks interface {
+	// OperationUpdate notifies FireFly of an update to an operation.
+	// Only success/failure and errorMessage (for errors) are modeled.
+	// opOutput can be used to add opaque protocol-specific JSON from the plugin (protocol transaction ID etc.)
+	// Note this is an optional hook information, and stored separately to the confirmation of the actual event that was being submitted/sequenced.
+	// Only the party submitting the transaction will see this data.
+	OperationUpdate(plugin Named, nsOpID string, status OpStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject)
+}
diff --git a/pkg/core/tokenpool.go b/pkg/core/tokenpool.go
index b8cf10b43..c858bc5d2 100644
--- a/pkg/core/tokenpool.go
+++ b/pkg/core/tokenpool.go
@@ -66,9 +66,6 @@ type TokenPoolAnnouncement struct {
 }
 
 func (t *TokenPool) Validate(ctx context.Context) (err error) {
-	if err = fftypes.ValidateFFNameField(ctx, t.Namespace, "namespace"); err != nil {
-		return err
-	}
 	if err = fftypes.ValidateFFNameFieldNoUUID(ctx, t.Name, "name"); err != nil {
 		return err
 	}
diff --git a/pkg/tokens/plugin.go b/pkg/tokens/plugin.go
index d9cf249b5..7982a58c9 100644
--- a/pkg/tokens/plugin.go
+++ b/pkg/tokens/plugin.go
@@ -39,6 +39,10 @@ type Plugin interface {
 	// If namespace is set, plugin will attempt to deliver only events for that namespace
 	SetHandler(namespace string, handler Callbacks) error
 
+	// SetOperationHandler registers a handler to receive async operation status
+	// If namespace is set, plugin will attempt to deliver only events for that namespace
+	SetOperationHandler(namespace string, handler core.OperationCallbacks)
+
 	// Blockchain interface must not deliver any events until start is called
 	Start() error
 
@@ -70,15 +74,6 @@ type Plugin interface {
 // has completed. However, it does not matter if these events are workload balance between the firefly core
 // cluster instances of the node.
 type Callbacks interface {
-	// TokenOpUpdate notifies firefly of an update to this plugin's operation within a transaction.
-	// Only success/failure and errorMessage (for errors) are modeled.
-	// opOutput can be used to add opaque protocol specific JSON from the plugin (protocol transaction ID etc.)
-	// Note this is an optional hook information, and stored separately to the confirmation of the actual event that was being submitted/sequenced.
-	// Only the party submitting the transaction will see this data.
-	//
-	// Error should only be returned in shutdown scenarios
-	TokenOpUpdate(plugin Plugin, nsOpID string, txState core.OpStatus, blockchainTXID, errorMessage string, opOutput fftypes.JSONObject)
-
 	// TokenPoolCreated notifies on the creation of a new token pool, which might have been
 	// submitted by us, or by any other authorized party in the network.
 	//


Test patch:
diff --git a/internal/batch/batch_manager_test.go b/internal/batch/batch_manager_test.go
index 231e5d1b0..4440af911 100644
--- a/internal/batch/batch_manager_test.go
+++ b/internal/batch/batch_manager_test.go
@@ -60,7 +60,7 @@ func TestE2EDispatchBroadcast(t *testing.T) {
 	mdm := &datamocks.Manager{}
 	mni := &sysmessagingmocks.LocalNodeInfo{}
 	txHelper := txcommon.NewTransactionHelper("ns1", mdi, mdm)
-	mni.On("GetNodeUUID", mock.Anything, "ns1").Return(fftypes.NewUUID())
+	mni.On("GetNodeUUID", mock.Anything).Return(fftypes.NewUUID())
 	readyForDispatch := make(chan bool)
 	waitForDispatch := make(chan *DispatchState)
 	handler := func(ctx context.Context, state *DispatchState) error {
@@ -176,7 +176,7 @@ func TestE2EDispatchPrivateUnpinned(t *testing.T) {
 	mdm := &datamocks.Manager{}
 	mni := &sysmessagingmocks.LocalNodeInfo{}
 	txHelper := txcommon.NewTransactionHelper("ns1", mdi, mdm)
-	mni.On("GetNodeUUID", mock.Anything, "ns1").Return(fftypes.NewUUID())
+	mni.On("GetNodeUUID", mock.Anything).Return(fftypes.NewUUID())
 	readyForDispatch := make(chan bool)
 	waitForDispatch := make(chan *DispatchState)
 	var groupID fftypes.Bytes32
@@ -320,7 +320,7 @@ func TestGetInvalidBatchTypeMsg(t *testing.T) {
 	txHelper := txcommon.NewTransactionHelper("ns1", mdi, mdm)
 	bm, _ := NewBatchManager(context.Background(), "ns1", mni, mdi, mdm, txHelper)
 	defer bm.Close()
-	_, err := bm.(*batchManager).getProcessor(core.BatchTypeBroadcast, "wrong", nil, "ns1", &core.SignerRef{})
+	_, err := bm.(*batchManager).getProcessor(core.BatchTypeBroadcast, "wrong", nil, &core.SignerRef{})
 	assert.Regexp(t, "FF10126", err)
 }
 
@@ -386,7 +386,7 @@ func TestMessageSequencerUpdateMessagesFail(t *testing.T) {
 	mdm := &datamocks.Manager{}
 	mni := &sysmessagingmocks.LocalNodeInfo{}
 	txHelper := txcommon.NewTransactionHelper("ns1", mdi, mdm)
-	mni.On("GetNodeUUID", mock.Anything, "ns1").Return(fftypes.NewUUID())
+	mni.On("GetNodeUUID", mock.Anything).Return(fftypes.NewUUID())
 	ctx, cancelCtx := context.WithCancel(context.Background())
 	bm, _ := NewBatchManager(ctx, "ns1", mni, mdi, mdm, txHelper)
 	bm.RegisterDispatcher("utdispatcher", core.TransactionTypeBatchPin, []core.MessageType{core.MessageTypeBroadcast},
@@ -440,7 +440,7 @@ func TestMessageSequencerDispatchFail(t *testing.T) {
 	mdi := &databasemocks.Plugin{}
 	mdm := &datamocks.Manager{}
 	mni := &sysmessagingmocks.LocalNodeInfo{}
-	mni.On("GetNodeUUID", mock.Anything, "ns1").Return(fftypes.NewUUID())
+	mni.On("GetNodeUUID", mock.Anything).Return(fftypes.NewUUID())
 	txHelper := txcommon.NewTransactionHelper("ns1", mdi, mdm)
 	ctx, cancelCtx := context.WithCancel(context.Background())
 	bm, _ := NewBatchManager(ctx, "ns1", mni, mdi, mdm, txHelper)
@@ -480,7 +480,7 @@ func TestMessageSequencerUpdateBatchFail(t *testing.T) {
 	mdi := &databasemocks.Plugin{}
 	mdm := &datamocks.Manager{}
 	mni := &sysmessagingmocks.LocalNodeInfo{}
-	mni.On("GetNodeUUID", mock.Anything, "ns1").Return(fftypes.NewUUID())
+	mni.On("GetNodeUUID", mock.Anything).Return(fftypes.NewUUID())
 	ctx, cancelCtx := context.WithCancel(context.Background())
 	txHelper := txcommon.NewTransactionHelper("ns1", mdi, mdm)
 	bm, _ := NewBatchManager(ctx, "ns1", mni, mdi, mdm, txHelper)
diff --git a/internal/batch/batch_processor_test.go b/internal/batch/batch_processor_test.go
index e4fb45823..4463db68f 100644
--- a/internal/batch/batch_processor_test.go
+++ b/internal/batch/batch_processor_test.go
@@ -42,12 +42,11 @@ func newTestBatchProcessor(t *testing.T, dispatch DispatchHandler) (func(), *dat
 	mni := bm.ni.(*sysmessagingmocks.LocalNodeInfo)
 	mdm := bm.data.(*datamocks.Manager)
 	txHelper := txcommon.NewTransactionHelper("ns1", mdi, mdm)
-	mni.On("GetNodeUUID", mock.Anything, "ns1").Return(fftypes.NewUUID()).Maybe()
+	mni.On("GetNodeUUID", mock.Anything).Return(fftypes.NewUUID()).Maybe()
 	bp := newBatchProcessor(bm, &batchProcessorConf{
-		namespace: "ns1",
-		txType:    core.TransactionTypeBatchPin,
-		signer:    core.SignerRef{Author: "did:firefly:org/abcd", Key: "0x12345"},
-		dispatch:  dispatch,
+		txType:   core.TransactionTypeBatchPin,
+		signer:   core.SignerRef{Author: "did:firefly:org/abcd", Key: "0x12345"},
+		dispatch: dispatch,
 		DispatcherOptions: DispatcherOptions{
 			BatchMaxSize:   10,
 			BatchMaxBytes:  1024 * 1024,
diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go
index 778f0d623..1e3127bf5 100644
--- a/internal/blockchain/ethereum/ethereum_test.go
+++ b/internal/blockchain/ethereum/ethereum_test.go
@@ -32,6 +32,7 @@ import (
 	"github.com/hyperledger/firefly-common/pkg/wsclient"
 	"github.com/hyperledger/firefly/internal/coreconfig"
 	"github.com/hyperledger/firefly/mocks/blockchainmocks"
+	"github.com/hyperledger/firefly/mocks/coremocks"
 	"github.com/hyperledger/firefly/mocks/metricsmocks"
 	"github.com/hyperledger/firefly/mocks/wsmocks"
 	"github.com/hyperledger/firefly/pkg/blockchain"
@@ -90,6 +91,7 @@ func newTestEthereum() (*Ethereum, func()) {
 		metrics:     mm,
 	}
 	e.callbacks.handlers = make(map[string]blockchain.Callbacks)
+	e.callbacks.opHandlers = make(map[string]core.OperationCallbacks)
 	return e, func() {
 		cancel()
 		if e.closed != nil {
@@ -884,8 +886,9 @@ func TestHandleMessageBatchPinOK(t *testing.T) {
 
 	em := &blockchainmocks.Callbacks{}
 	e := &Ethereum{
-		callbacks: callbacks{handlers: map[string]blockchain.Callbacks{"ns1": em}},
+		callbacks: callbacks{handlers: make(map[string]blockchain.Callbacks)},
 	}
+	e.SetHandler("ns1", em)
 
 	e.subs = map[string]subscriptionInfo{}
 	e.subs["sb-b5b97a4e-a317-4053-6400-1474650efcb5"] = subscriptionInfo{
@@ -1454,12 +1457,12 @@ func TestEventLoopSendClosed(t *testing.T) {
 }
 
 func TestHandleReceiptTXSuccess(t *testing.T) {
-	em := &blockchainmocks.Callbacks{}
+	em := &coremocks.OperationCallbacks{}
 	wsm := &wsmocks.WSClient{}
 	e := &Ethereum{
 		ctx:       context.Background(),
 		topic:     "topic1",
-		callbacks: callbacks{handlers: map[string]blockchain.Callbacks{"ns1": em}},
+		callbacks: callbacks{opHandlers: map[string]core.OperationCallbacks{"ns1": em}},
 		wsconn:    wsm,
 	}
 
@@ -1488,7 +1491,7 @@ func TestHandleReceiptTXSuccess(t *testing.T) {
 		"transactionIndex": "0"
   }`)
 
-	em.On("BlockchainOpUpdate",
+	em.On("OperationUpdate",
 		e,
 		"ns1:"+operationID.String(),
 		core.OpStatusSucceeded,
@@ -1528,9 +1531,9 @@ func TestHandleBadPayloadsAndThenReceiptFailure(t *testing.T) {
 		"requestPayload": "{\"from\":\"0x91d2b4381a4cd5c7c0f27565a7d4b829844c8635\",\"gas\":0,\"gasPrice\":0,\"headers\":{\"id\":\"6fb94fff-81d3-4094-567d-e031b1871694\",\"type\":\"SendTransaction\"},\"method\":{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"txnId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"batchId\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"payloadRef\",\"type\":\"bytes32\"}],\"name\":\"broadcastBatch\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},\"params\":[\"12345\",\"!\",\"!\"],\"to\":\"0xd3266a857285fb75eb7df37353b4a15c8bb828f5\",\"value\":0}"
 	}`)
 
-	em := &blockchainmocks.Callbacks{}
-	e.SetHandler("ns1", em)
-	txsu := em.On("BlockchainOpUpdate",
+	em := &coremocks.OperationCallbacks{}
+	e.SetOperationHandler("ns1", em)
+	txsu := em.On("OperationUpdate",
 		e,
 		"ns1:"+operationID.String(),
 		core.OpStatusFailed,
@@ -3172,7 +3175,7 @@ func TestRemoveInvalidSubscription(t *testing.T) {
 func TestCallbacksWrongNamespace(t *testing.T) {
 	e, _ := newTestEthereum()
 	nsOpID := "ns1:" + fftypes.NewUUID().String()
-	e.callbacks.BlockchainOpUpdate(context.Background(), e, nsOpID, core.OpStatusSucceeded, "tx123", "", nil)
+	e.callbacks.OperationUpdate(context.Background(), e, nsOpID, core.OpStatusSucceeded, "tx123", "", nil)
 	e.callbacks.BatchPinComplete(context.Background(), &blockchain.BatchPin{Namespace: "ns1"}, nil)
 	e.callbacks.BlockchainNetworkAction(context.Background(), "ns1", "terminate", nil, nil, nil)
 }
diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go
index 5e5e55bfd..6962d2b17 100644
--- a/internal/blockchain/fabric/fabric_test.go
+++ b/internal/blockchain/fabric/fabric_test.go
@@ -33,6 +33,7 @@ import (
 	"github.com/hyperledger/firefly-common/pkg/wsclient"
 	"github.com/hyperledger/firefly/internal/coreconfig"
 	"github.com/hyperledger/firefly/mocks/blockchainmocks"
+	"github.com/hyperledger/firefly/mocks/coremocks"
 	"github.com/hyperledger/firefly/mocks/metricsmocks"
 	"github.com/hyperledger/firefly/mocks/wsmocks"
 	"github.com/hyperledger/firefly/pkg/blockchain"
@@ -1387,14 +1388,15 @@ func TestEventLoopUnexpectedMessage(t *testing.T) {
 }
 
 func TestHandleReceiptTXSuccess(t *testing.T) {
-	em := &blockchainmocks.Callbacks{}
+	em := &coremocks.OperationCallbacks{}
 	wsm := &wsmocks.WSClient{}
 	e := &Fabric{
 		ctx:       context.Background(),
 		topic:     "topic1",
-		callbacks: callbacks{handlers: map[string]blockchain.Callbacks{"ns1": em}},
+		callbacks: callbacks{opHandlers: make(map[string]core.OperationCallbacks)},
 		wsconn:    wsm,
 	}
+	e.SetOperationHandler("ns1", em)
 
 	var reply fftypes.JSONObject
 	operationID := fftypes.NewUUID()
@@ -1412,7 +1414,7 @@ func TestHandleReceiptTXSuccess(t *testing.T) {
 		"receivedAt": 1630033474675
   }`)
 
-	em.On("BlockchainOpUpdate",
+	em.On("OperationUpdate",
 		e,
 		"ns1:"+operationID.String(),
 		core.OpStatusSucceeded,
@@ -1424,6 +1426,7 @@ func TestHandleReceiptTXSuccess(t *testing.T) {
 	assert.NoError(t, err)
 	e.handleReceipt(context.Background(), reply)
 
+	em.AssertExpectations(t)
 }
 
 func TestHandleReceiptNoRequestID(t *testing.T) {
@@ -1444,12 +1447,12 @@ func TestHandleReceiptNoRequestID(t *testing.T) {
 }
 
 func TestHandleReceiptFailedTx(t *testing.T) {
-	em := &blockchainmocks.Callbacks{}
+	em := &coremocks.OperationCallbacks{}
 	wsm := &wsmocks.WSClient{}
 	e := &Fabric{
 		ctx:       context.Background(),
 		topic:     "topic1",
-		callbacks: callbacks{handlers: map[string]blockchain.Callbacks{"ns1": em}},
+		callbacks: callbacks{opHandlers: map[string]core.OperationCallbacks{"ns1": em}},
 		wsconn:    wsm,
 	}
 
@@ -1469,7 +1472,7 @@ func TestHandleReceiptFailedTx(t *testing.T) {
 		"transactionId": "ce79343000e851a0c742f63a733ce19a5f8b9ce1c719b6cecd14f01bcf81fff2"
   }`)
 
-	em.On("BlockchainOpUpdate",
+	em.On("OperationUpdate",
 		e,
 		"ns1:"+operationID.String(),
 		core.OpStatusFailed,
@@ -1480,6 +1483,8 @@ func TestHandleReceiptFailedTx(t *testing.T) {
 	err := json.Unmarshal(data, &reply)
 	assert.NoError(t, err)
 	e.handleReceipt(context.Background(), reply)
+
+	em.AssertExpectations(t)
 }
 
 func TestFormatNil(t *testing.T) {
@@ -2697,7 +2702,7 @@ func TestSubmitNetworkActionBadLocation(t *testing.T) {
 func TestCallbacksWrongNamespace(t *testing.T) {
 	e, _ := newTestFabric()
 	nsOpID := "ns1:" + fftypes.NewUUID().String()
-	e.callbacks.BlockchainOpUpdate(context.Background(), e, nsOpID, core.OpStatusSucceeded, "tx123", "", nil)
+	e.callbacks.OperationUpdate(context.Background(), e, nsOpID, core.OpStatusSucceeded, "tx123", "", nil)
 	e.callbacks.BatchPinComplete(context.Background(), &blockchain.BatchPin{Namespace: "ns1"}, nil)
 	e.callbacks.BlockchainNetworkAction(context.Background(), "ns1", "terminate", nil, nil, nil)
 }
diff --git a/internal/broadcast/manager_test.go b/internal/broadcast/manager_test.go
index 6ef2cbe24..f879f8c89 100644
--- a/internal/broadcast/manager_test.go
+++ b/internal/broadcast/manager_test.go
@@ -76,7 +76,8 @@ func newTestBroadcastCommon(t *testing.T, metricsEnabled bool) (*broadcastManage
 	}
 
 	ctx, cancel := context.WithCancel(context.Background())
-	b, err := NewBroadcastManager(ctx, "ns1", mdi, mbi, mdx, mpi, mim, mdm, mba, msa, mmp, mmi, mom)
+	ns := core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"}
+	b, err := NewBroadcastManager(ctx, ns, mdi, mbi, mdx, mpi, mim, mdm, mba, msa, mmp, mmi, mom)
 	assert.NoError(t, err)
 	return b.(*broadcastManager), cancel
 }
@@ -93,7 +94,7 @@ func newTestBroadcastWithMetrics(t *testing.T) (*broadcastManager, func()) {
 }
 
 func TestInitFail(t *testing.T) {
-	_, err := NewBroadcastManager(context.Background(), "ns1", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
+	_, err := NewBroadcastManager(context.Background(), core.NamespaceRef{}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
 	assert.Regexp(t, "FF10128", err)
 }
 
diff --git a/internal/broadcast/operations_test.go b/internal/broadcast/operations_test.go
index d79718f73..7b5ece3bc 100644
--- a/internal/broadcast/operations_test.go
+++ b/internal/broadcast/operations_test.go
@@ -61,7 +61,7 @@ func TestPrepareAndRunBatchBroadcast(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, bp.ID, po.Data.(uploadBatchData).Batch.ID)
 
-	_, complete, err := bm.RunOperation(context.Background(), opUploadBatch(op, batch, bp))
+	_, complete, err := bm.RunOperation(context.Background(), opUploadBatch(op, batch))
 
 	assert.True(t, complete)
 	assert.NoError(t, err)
@@ -178,7 +178,7 @@ func TestRunOperationBatchBroadcastInvalidData(t *testing.T) {
 		},
 	}
 
-	_, complete, err := bm.RunOperation(context.Background(), opUploadBatch(op, batch, &core.BatchPersisted{}))
+	_, complete, err := bm.RunOperation(context.Background(), opUploadBatch(op, batch))
 
 	assert.False(t, complete)
 	assert.Regexp(t, "FF10137", err)
@@ -198,7 +198,7 @@ func TestRunOperationBatchBroadcastPublishFail(t *testing.T) {
 	mps := bm.sharedstorage.(*sharedstoragemocks.Plugin)
 	mps.On("UploadData", context.Background(), mock.Anything).Return("", fmt.Errorf("pop"))
 
-	_, complete, err := bm.RunOperation(context.Background(), opUploadBatch(op, batch, &core.BatchPersisted{}))
+	_, complete, err := bm.RunOperation(context.Background(), opUploadBatch(op, batch))
 
 	assert.False(t, complete)
 	assert.EqualError(t, err, "pop")
@@ -221,8 +221,7 @@ func TestRunOperationBatchBroadcast(t *testing.T) {
 	mdi := bm.database.(*databasemocks.Plugin)
 	mps.On("UploadData", context.Background(), mock.Anything).Return("123", nil)
 
-	bp := &core.BatchPersisted{}
-	outputs, complete, err := bm.RunOperation(context.Background(), opUploadBatch(op, batch, bp))
+	outputs, complete, err := bm.RunOperation(context.Background(), opUploadBatch(op, batch))
 	assert.Equal(t, "123", outputs["payloadRef"])
 
 	assert.True(t, complete)
diff --git a/internal/data/data_manager_test.go b/internal/data/data_manager_test.go
index 7870cd85a..86477172b 100644
--- a/internal/data/data_manager_test.go
+++ b/internal/data/data_manager_test.go
@@ -41,7 +41,8 @@ func newTestDataManager(t *testing.T) (*dataManager, context.Context, func()) {
 		Concurrency: true,
 	})
 	mdx := &dataexchangemocks.Plugin{}
-	dm, err := NewDataManager(ctx, "ns1", mdi, mdx)
+	ns := core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"}
+	dm, err := NewDataManager(ctx, ns, mdi, mdx)
 	assert.NoError(t, err)
 	return dm.(*dataManager), ctx, func() {
 		cancel()
@@ -211,7 +212,7 @@ func TestWriteNewMessageE2E(t *testing.T) {
 }
 
 func TestInitBadDeps(t *testing.T) {
-	_, err := NewDataManager(context.Background(), "", nil, nil)
+	_, err := NewDataManager(context.Background(), core.NamespaceRef{}, nil, nil)
 	assert.Regexp(t, "FF10128", err)
 }
 
diff --git a/internal/database/sqlcommon/group_sql_test.go b/internal/database/sqlcommon/group_sql_test.go
index 8ce1defd5..ee01168f1 100644
--- a/internal/database/sqlcommon/group_sql_test.go
+++ b/internal/database/sqlcommon/group_sql_test.go
@@ -49,8 +49,9 @@ func TestUpsertGroupE2EWithDB(t *testing.T) {
 				{Identity: "0x23456", Node: fftypes.NewUUID()},
 			},
 		},
-		Hash:    groupHash,
-		Created: fftypes.Now(),
+		LocalNamespace: "ns1",
+		Hash:           groupHash,
+		Created:        fftypes.Now(),
 	}
 
 	s.callbacks.On("HashCollectionNSEvent", database.CollectionGroups, core.ChangeEventTypeCreated, "ns1", groupHash, mock.Anything).Return()
@@ -74,9 +75,10 @@ func TestUpsertGroupE2EWithDB(t *testing.T) {
 			Namespace: "ns1",
 			Members:   group.Members,
 		},
-		Created: fftypes.Now(),
-		Message: fftypes.NewUUID(),
-		Hash:    groupHash,
+		LocalNamespace: "ns1",
+		Created:        fftypes.Now(),
+		Message:        fftypes.NewUUID(),
+		Hash:           groupHash,
 	}
 
 	err = s.UpsertGroup(context.Background(), groupUpdated, database.UpsertOptimizationExisting)
@@ -325,7 +327,7 @@ func TestGetGroupByIDLoadMembersFail(t *testing.T) {
 	s, mock := newMockProvider().init()
 	groupID := fftypes.NewRandB32()
 	mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows(groupColumns).
-		AddRow(nil, "ns1", "name1", fftypes.NewRandB32(), fftypes.Now()))
+		AddRow(nil, "ns1", "ns1", "name1", fftypes.NewRandB32(), fftypes.Now()))
 	mock.ExpectQuery("SELECT .*").WillReturnError(fmt.Errorf("pop"))
 	_, err := s.GetGroupByHash(context.Background(), "ns1", groupID)
 	assert.Regexp(t, "FF10115", err)
@@ -360,7 +362,7 @@ func TestGetGroupsReadGroupFail(t *testing.T) {
 func TestGetGroupsLoadMembersFail(t *testing.T) {
 	s, mock := newMockProvider().init()
 	mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows(groupColumns).
-		AddRow(nil, "ns1", "group1", fftypes.NewRandB32(), fftypes.Now()))
+		AddRow(nil, "ns1", "ns1", "group1", fftypes.NewRandB32(), fftypes.Now()))
 	mock.ExpectQuery("SELECT .*").WillReturnError(fmt.Errorf("pop"))
 	f := database.GroupQueryFactory.NewFilter(context.Background()).Gt("created", "0")
 	_, _, err := s.GetGroups(context.Background(), "ns1", f)
diff --git a/internal/database/sqlcommon/message_sql_test.go b/internal/database/sqlcommon/message_sql_test.go
index 75b51df82..9b78a820b 100644
--- a/internal/database/sqlcommon/message_sql_test.go
+++ b/internal/database/sqlcommon/message_sql_test.go
@@ -46,6 +46,7 @@ func TestUpsertE2EWithDB(t *testing.T) {
 	rand1 := fftypes.NewRandB32()
 	rand2 := fftypes.NewRandB32()
 	msg := &core.Message{
+		LocalNamespace: "ns12345",
 		Header: core.MessageHeader{
 			ID:   msgID,
 			CID:  nil,
@@ -92,6 +93,7 @@ func TestUpsertE2EWithDB(t *testing.T) {
 	gid := fftypes.NewRandB32()
 	bid := fftypes.NewUUID()
 	msgUpdated := &core.Message{
+		LocalNamespace: "ns12345",
 		Header: core.MessageHeader{
 			ID:   msgID,
 			CID:  cid,
@@ -310,8 +312,8 @@ func TestInsertMessagesMultiRowOK(t *testing.T) {
 	s.features.MultiRowInsert = true
 	s.fakePSQLInsert = true
 
-	msg1 := &core.Message{Header: core.MessageHeader{ID: fftypes.NewUUID(), Namespace: "ns1"}, Data: core.DataRefs{{ID: fftypes.NewUUID()}}}
-	msg2 := &core.Message{Header: core.MessageHeader{ID: fftypes.NewUUID(), Namespace: "ns1"}, Data: core.DataRefs{{ID: fftypes.NewUUID()}}}
+	msg1 := &core.Message{Header: core.MessageHeader{ID: fftypes.NewUUID(), Namespace: "ns1"}, LocalNamespace: "ns1", Data: core.DataRefs{{ID: fftypes.NewUUID()}}}
+	msg2 := &core.Message{Header: core.MessageHeader{ID: fftypes.NewUUID(), Namespace: "ns1"}, LocalNamespace: "ns1", Data: core.DataRefs{{ID: fftypes.NewUUID()}}}
 	s.callbacks.On("OrderedUUIDCollectionNSEvent", database.CollectionMessages, core.ChangeEventTypeCreated, "ns1", msg1.Header.ID, int64(1001))
 	s.callbacks.On("OrderedUUIDCollectionNSEvent", database.CollectionMessages, core.ChangeEventTypeCreated, "ns1", msg2.Header.ID, int64(1002))
 
@@ -546,7 +548,7 @@ func TestGetMessageByIDLoadRefsFail(t *testing.T) {
 	cols := append([]string{}, msgColumns...)
 	cols = append(cols, "id()")
 	mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows(cols).
-		AddRow(msgID.String(), nil, core.MessageTypeBroadcast, "author1", "0x12345", 0, "ns1", "t1", "c1", nil, b32.String(), b32.String(), b32.String(), "confirmed", 0, "pin", nil, 0))
+		AddRow(msgID.String(), nil, core.MessageTypeBroadcast, "author1", "0x12345", 0, "ns1", "ns1", "t1", "c1", nil, b32.String(), b32.String(), b32.String(), "confirmed", 0, "pin", nil, 0))
 	mock.ExpectQuery("SELECT .*").WillReturnError(fmt.Errorf("pop"))
 	_, err := s.GetMessageByID(context.Background(), "ns1", msgID)
 	assert.Regexp(t, "FF10115", err)
@@ -593,7 +595,7 @@ func TestGetMessagesLoadRefsFail(t *testing.T) {
 	cols := append([]string{}, msgColumns...)
 	cols = append(cols, "id()")
 	mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows(cols).
-		AddRow(msgID.String(), nil, core.MessageTypeBroadcast, "author1", "0x12345", 0, "ns1", "t1", "c1", nil, b32.String(), b32.String(), b32.String(), "confirmed", 0, "pin", nil, 0))
+		AddRow(msgID.String(), nil, core.MessageTypeBroadcast, "author1", "0x12345", 0, "ns1", "ns1", "t1", "c1", nil, b32.String(), b32.String(), b32.String(), "confirmed", 0, "pin", nil, 0))
 	mock.ExpectQuery("SELECT .*").WillReturnError(fmt.Errorf("pop"))
 	f := database.MessageQueryFactory.NewFilter(context.Background()).Gt("confirmed", "0")
 	_, _, err := s.GetMessages(context.Background(), "ns1", f)
diff --git a/internal/definitions/handler_contracts_test.go b/internal/definitions/handler_contracts_test.go
index 14d013740..d5cb0ee37 100644
--- a/internal/definitions/handler_contracts_test.go
+++ b/internal/definitions/handler_contracts_test.go
@@ -215,7 +215,6 @@ func TestHandleFFIBroadcastPersistFail(t *testing.T) {
 	}
 	mdi := dh.database.(*databasemocks.Plugin)
 	mdi.On("UpsertFFI", mock.Anything, mock.Anything).Return(fmt.Errorf("pop"))
-	mdi.On("InsertEvent", mock.Anything, mock.Anything).Return(nil)
 	mcm := dh.contracts.(*contractmocks.Manager)
 	mcm.On("ResolveFFI", mock.Anything, mock.Anything).Return(nil)
 	action, err := dh.HandleDefinitionBroadcast(context.Background(), &bs.BatchState, &core.Message{
@@ -226,6 +225,31 @@ func TestHandleFFIBroadcastPersistFail(t *testing.T) {
 	assert.Equal(t, HandlerResult{Action: ActionRetry}, action)
 	assert.Regexp(t, "pop", err)
 	bs.assertNoFinalizers()
+
+	mdi.AssertExpectations(t)
+	mcm.AssertExpectations(t)
+}
+
+func TestHandleFFIBroadcastResolveFail(t *testing.T) {
+	dh, bs := newTestDefinitionHandler(t)
+	ffi := testFFI()
+	b, err := json.Marshal(ffi)
+	assert.NoError(t, err)
+	data := &core.Data{
+		Value: fftypes.JSONAnyPtrBytes(b),
+	}
+	mcm := dh.contracts.(*contractmocks.Manager)
+	mcm.On("ResolveFFI", mock.Anything, mock.Anything).Return(fmt.Errorf("pop"))
+	action, err := dh.HandleDefinitionBroadcast(context.Background(), &bs.BatchState, &core.Message{
+		Header: core.MessageHeader{
+			Tag: core.SystemTagDefineFFI,
+		},
+	}, core.DataArray{data}, fftypes.NewUUID())
+	assert.Equal(t, HandlerResult{Action: ActionReject}, action)
+	assert.Regexp(t, "pop", err)
+	bs.assertNoFinalizers()
+
+	mcm.AssertExpectations(t)
 }
 
 func TestHandleContractAPIBroadcastOk(t *testing.T) {
@@ -359,3 +383,27 @@ func TestHandleContractAPIBroadcastPersistFail(t *testing.T) {
 	mdi.AssertExpectations(t)
 	mcm.AssertExpectations(t)
 }
+
+func TestHandleContractAPIBroadcastResolveFail(t *testing.T) {
+	dh, bs := newTestDefinitionHandler(t)
+	ffi := testFFI()
+	b, err := json.Marshal(ffi)
+	assert.NoError(t, err)
+	data := &core.Data{
+		Value: fftypes.JSONAnyPtrBytes(b),
+	}
+	mcm := dh.contracts.(*contractmocks.Manager)
+	mcm.On("ResolveContractAPI", context.Background(), "", mock.Anything).Return(fmt.Errorf("pop"))
+
+	action, err := dh.HandleDefinitionBroadcast(context.Background(), &bs.BatchState, &core.Message{
+		Header: core.MessageHeader{
+			Tag: core.SystemTagDefineContractAPI,
+		},
+	}, core.DataArray{data}, fftypes.NewUUID())
+	assert.Equal(t, HandlerResult{Action: ActionReject}, action)
+	assert.Regexp(t, "pop", err)
+
+	bs.assertNoFinalizers()
+
+	mcm.AssertExpectations(t)
+}
diff --git a/internal/definitions/handler_network_node_test.go b/internal/definitions/handler_network_node_test.go
index 258922008..8eacff79b 100644
--- a/internal/definitions/handler_network_node_test.go
+++ b/internal/definitions/handler_network_node_test.go
@@ -110,9 +110,9 @@ func TestHandleDeprecatedNodeDefinitionOK(t *testing.T) {
 	mim.On("VerifyIdentityChain", ctx, mock.Anything).Return(parent.Migrated().Identity, false, nil)
 
 	mdi := dh.database.(*databasemocks.Plugin)
-	mdi.On("GetIdentityByName", ctx, core.IdentityTypeNode, core.LegacySystemNamespace, node.Name).Return(nil, nil)
+	mdi.On("GetIdentityByName", ctx, core.IdentityTypeNode, "ns1", node.Name).Return(nil, nil)
 	mdi.On("GetIdentityByID", ctx, "ns1", node.ID).Return(nil, nil)
-	mdi.On("GetVerifierByValue", ctx, core.VerifierTypeFFDXPeerID, core.LegacySystemNamespace, "member_0").Return(nil, nil)
+	mdi.On("GetVerifierByValue", ctx, core.VerifierTypeFFDXPeerID, "ns1", "member_0").Return(nil, nil)
 	mdi.On("UpsertIdentity", ctx, mock.MatchedBy(func(identity *core.Identity) bool {
 		assert.Equal(t, *msg.Header.ID, *identity.Messages.Claim)
 		return true
diff --git a/internal/definitions/handler_network_org_test.go b/internal/definitions/handler_network_org_test.go
index 7b4a7928c..c226f0604 100644
--- a/internal/definitions/handler_network_org_test.go
+++ b/internal/definitions/handler_network_org_test.go
@@ -93,9 +93,9 @@ func TestHandleDeprecatedOrgDefinitionOK(t *testing.T) {
 	mim.On("VerifyIdentityChain", ctx, mock.Anything).Return(nil, false, nil)
 
 	mdi := dh.database.(*databasemocks.Plugin)
-	mdi.On("GetIdentityByName", ctx, core.IdentityTypeOrg, core.LegacySystemNamespace, org.Name).Return(nil, nil)
+	mdi.On("GetIdentityByName", ctx, core.IdentityTypeOrg, "ns1", org.Name).Return(nil, nil)
 	mdi.On("GetIdentityByID", ctx, "ns1", org.ID).Return(nil, nil)
-	mdi.On("GetVerifierByValue", ctx, core.VerifierTypeEthAddress, core.LegacySystemNamespace, msg.Header.Key).Return(nil, nil)
+	mdi.On("GetVerifierByValue", ctx, core.VerifierTypeEthAddress, "ns1", msg.Header.Key).Return(nil, nil)
 	mdi.On("UpsertIdentity", ctx, mock.MatchedBy(func(identity *core.Identity) bool {
 		assert.Equal(t, *msg.Header.ID, *identity.Messages.Claim)
 		return true
diff --git a/internal/definitions/sender_contracts_test.go b/internal/definitions/sender_contracts_test.go
index 203b2829c..e393222cf 100644
--- a/internal/definitions/sender_contracts_test.go
+++ b/internal/definitions/sender_contracts_test.go
@@ -100,17 +100,11 @@ func TestDefineFFIOk(t *testing.T) {
 func TestDefineFFINonMultiparty(t *testing.T) {
 	ds, cancel := newTestDefinitionSender(t)
 	defer cancel()
-	dh := ds.handler
 
 	ffi := &fftypes.FFI{}
 
-	mcm := dh.contracts.(*contractmocks.Manager)
-	mcm.On("ResolveFFI", context.Background(), ffi).Return(fmt.Errorf("pop"))
-
 	err := ds.DefineFFI(context.Background(), ffi, false)
 	assert.Regexp(t, "FF10403", err)
-
-	mcm.AssertExpectations(t)
 }
 
 func TestDefineContractAPIResolveFail(t *testing.T) {
@@ -182,16 +176,10 @@ func TestDefineContractAPIOk(t *testing.T) {
 func TestDefineContractAPINonMultiparty(t *testing.T) {
 	ds, cancel := newTestDefinitionSender(t)
 	defer cancel()
-	dh := ds.handler
 
 	url := "http://firefly"
 	api := &core.ContractAPI{}
 
-	mcm := dh.contracts.(*contractmocks.Manager)
-	mcm.On("ResolveContractAPI", context.Background(), url, api).Return(fmt.Errorf("pop"))
-
 	err := ds.DefineContractAPI(context.Background(), url, api, false)
 	assert.Regexp(t, "FF10403", err)
-
-	mcm.AssertExpectations(t)
 }
diff --git a/internal/events/batch_pin_complete_test.go b/internal/events/batch_pin_complete_test.go
index 4d57d94fe..5213a3b48 100644
--- a/internal/events/batch_pin_complete_test.go
+++ b/internal/events/batch_pin_complete_test.go
@@ -383,25 +383,6 @@ func TestBatchPinCompleteNoTX(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-func TestBatchPinCompleteBadNamespace(t *testing.T) {
-	em, cancel := newTestEventManager(t)
-	defer cancel()
-
-	batch := &blockchain.BatchPin{
-		Namespace:     "!bad",
-		TransactionID: fftypes.NewUUID(),
-		Event: blockchain.Event{
-			BlockchainTXID: "0x12345",
-		},
-	}
-
-	err := em.BatchPinComplete(batch, &core.VerifierRef{
-		Type:  core.VerifierTypeEthAddress,
-		Value: "0x12345",
-	})
-	assert.NoError(t, err)
-}
-
 func TestBatchPinCompleteWrongNamespace(t *testing.T) {
 	em, cancel := newTestEventManager(t)
 	defer cancel()
diff --git a/internal/events/dx_callbacks_test.go b/internal/events/dx_callbacks_test.go
index 70f5366c3..ad45bbd46 100644
--- a/internal/events/dx_callbacks_test.go
+++ b/internal/events/dx_callbacks_test.go
@@ -240,7 +240,7 @@ func TestMessageReceivePersistBatchError(t *testing.T) {
 func TestMessageReceivedWrongNS(t *testing.T) {
 	em, cancel := newTestEventManager(t)
 	defer cancel()
-	em.namespace = "ns2"
+	em.namespace.RemoteName = "ns2"
 
 	_, b := sampleBatchTransfer(t, core.TransactionTypeBatchPin)
 
@@ -458,7 +458,7 @@ func TestPrivateBlobReceivedGetBlobsFails(t *testing.T) {
 func TestPrivateBlobReceivedWrongNS(t *testing.T) {
 	em, cancel := newTestEventManager(t)
 	cancel() // retryable error
-	em.namespace = "ns2"
+	em.namespace.RemoteName = "ns2"
 	hash := fftypes.NewRandB32()
 
 	mdx := &dataexchangemocks.Plugin{}
diff --git a/internal/events/event_manager_test.go b/internal/events/event_manager_test.go
index 190989e8b..b2a9753bc 100644
--- a/internal/events/event_manager_test.go
+++ b/internal/events/event_manager_test.go
@@ -89,13 +89,14 @@ func newTestEventManagerCommon(t *testing.T, metrics, dbconcurrency bool) (*even
 	if metrics {
 		mmi.On("TransferConfirmed", mock.Anything)
 	}
-	mni.On("GetNodeUUID", mock.Anything, "ns1").Return(testNodeID).Maybe()
+	mni.On("GetNodeUUID", mock.Anything).Return(testNodeID).Maybe()
 	met.On("Name").Return("ut").Maybe()
 	mbi.On("VerifierType").Return(core.VerifierTypeEthAddress).Maybe()
 	mdi.On("Capabilities").Return(&database.Capabilities{Concurrency: dbconcurrency}).Maybe()
 	mev.On("SetHandler", "ns1", mock.Anything).Return(nil).Maybe()
 	mev.On("ValidateOptions", mock.Anything).Return(nil).Maybe()
-	emi, err := NewEventManager(ctx, "ns1", mni, mdi, mbi, mim, msh, mdm, mds, mbm, mpm, mam, mdd, mmi, txHelper, events, mmp)
+	ns := core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"}
+	emi, err := NewEventManager(ctx, ns, mni, mdi, mbi, mim, msh, mdm, mds, mbm, mpm, mam, mdd, mmi, txHelper, events, mmp)
 	em := emi.(*eventManager)
 	em.txHelper = &txcommonmocks.Helper{}
 	mockRunAsGroupPassthrough(mdi)
@@ -130,7 +131,7 @@ func TestStartStop(t *testing.T) {
 }
 
 func TestStartStopBadDependencies(t *testing.T) {
-	_, err := NewEventManager(context.Background(), "", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
+	_, err := NewEventManager(context.Background(), core.NamespaceRef{}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
 	assert.Regexp(t, "FF10128", err)
 
 }
@@ -157,7 +158,8 @@ func TestStartStopEventListenerFail(t *testing.T) {
 	mdi.On("Capabilities").Return(&database.Capabilities{Concurrency: false})
 	mbi.On("VerifierType").Return(core.VerifierTypeEthAddress)
 	mev.On("SetHandler", "ns1", mock.Anything).Return(fmt.Errorf("pop"))
-	_, err := NewEventManager(context.Background(), "ns1", mni, mdi, mbi, mim, msh, mdm, mds, mbm, mpm, mam, msd, mm, txHelper, events, mmp)
+	ns := core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"}
+	_, err := NewEventManager(context.Background(), ns, mni, mdi, mbi, mim, msh, mdm, mds, mbm, mpm, mam, msd, mm, txHelper, events, mmp)
 	assert.EqualError(t, err, "pop")
 }
 
diff --git a/internal/events/ss_callbacks_test.go b/internal/events/ss_callbacks_test.go
index a79963c40..c04a61379 100644
--- a/internal/events/ss_callbacks_test.go
+++ b/internal/events/ss_callbacks_test.go
@@ -98,7 +98,7 @@ func TestSharedStorageBatchDownloadedNSMismatch(t *testing.T) {
 	mss := &sharedstoragemocks.Plugin{}
 	mss.On("Name").Return("utdx").Maybe()
 
-	em.namespace = "ns2"
+	em.namespace.RemoteName = "ns2"
 	_, err := em.SharedStorageBatchDownloaded(mss, "payload1", b)
 	assert.NoError(t, err)
 
diff --git a/internal/events/token_pool_created_test.go b/internal/events/token_pool_created_test.go
index 8205f40c2..452ae1246 100644
--- a/internal/events/token_pool_created_test.go
+++ b/internal/events/token_pool_created_test.go
@@ -157,60 +157,6 @@ func TestTokenPoolCreatedConfirm(t *testing.T) {
 	mdm.AssertExpectations(t)
 }
 
-func TestTokenPoolCreatedConfirmWrongNS(t *testing.T) {
-	em, cancel := newTestEventManager(t)
-	defer cancel()
-	mdi := em.database.(*databasemocks.Plugin)
-	mti := &tokenmocks.Plugin{}
-	mth := em.txHelper.(*txcommonmocks.Helper)
-	mdm := em.data.(*datamocks.Manager)
-
-	txID := fftypes.NewUUID()
-	info1 := fftypes.JSONObject{"pool": "info"}
-	info2 := fftypes.JSONObject{"block": "info"}
-	chainPool := &tokens.TokenPool{
-		Type:        core.TokenTypeFungible,
-		PoolLocator: "123",
-		Connector:   "erc1155",
-		TX: core.TransactionRef{
-			ID:   txID,
-			Type: core.TransactionTypeTokenPool,
-		},
-		Standard: "ERC1155",
-		Symbol:   "FFT",
-		Info:     info1,
-		Event: blockchain.Event{
-			BlockchainTXID: "0xffffeeee",
-			Name:           "TokenPool",
-			ProtocolID:     "tx1",
-			Info:           info2,
-		},
-	}
-	storedPool := &core.TokenPool{
-		Namespace: "ns2",
-		ID:        fftypes.NewUUID(),
-		State:     core.TokenPoolStatePending,
-		Message:   fftypes.NewUUID(),
-		TX: core.TransactionRef{
-			Type: core.TransactionTypeTokenPool,
-			ID:   txID,
-		},
-	}
-
-	mdi.On("GetTokenPoolByLocator", em.ctx, "ns1", "erc1155", "123").Return(nil, fmt.Errorf("pop")).Once()
-	mdi.On("GetTokenPoolByLocator", em.ctx, "ns1", "erc1155", "123").Return(storedPool, nil).Once()
-	mth.On("InsertBlockchainEvent", em.ctx, mock.MatchedBy(func(e *core.BlockchainEvent) bool {
-		return e.Name == chainPool.Event.Name
-	})).Return(nil).Once()
-	mth.On("PersistTransaction", mock.Anything, txID, core.TransactionTypeTokenPool, "0xffffeeee").Return(true, nil).Once()
-
-	err := em.TokenPoolCreated(mti, chainPool)
-	assert.NoError(t, err)
-
-	mdi.AssertExpectations(t)
-	mdm.AssertExpectations(t)
-}
-
 func TestTokenPoolCreatedAlreadyConfirmed(t *testing.T) {
 	em, cancel := newTestEventManager(t)
 	defer cancel()
@@ -517,51 +463,6 @@ func TestTokenPoolCreatedAnnounce(t *testing.T) {
 	mds.AssertExpectations(t)
 }
 
-func TestTokenPoolCreatedAnnounceWrongNS(t *testing.T) {
-	em, cancel := newTestEventManager(t)
-	defer cancel()
-	mdi := em.database.(*databasemocks.Plugin)
-	mti := &tokenmocks.Plugin{}
-
-	poolID := fftypes.NewUUID()
-	txID := fftypes.NewUUID()
-	operations := []*core.Operation{
-		{
-			ID: fftypes.NewUUID(),
-			Input: fftypes.JSONObject{
-				"id":        poolID.String(),
-				"namespace": "ns2",
-				"name":      "my-pool",
-			},
-		},
-	}
-	info := fftypes.JSONObject{"some": "info"}
-	pool := &tokens.TokenPool{
-		Type:        core.TokenTypeFungible,
-		PoolLocator: "123",
-		TX: core.TransactionRef{
-			ID:   txID,
-			Type: core.TransactionTypeTokenPool,
-		},
-		Connector: "erc1155",
-		Event: blockchain.Event{
-			BlockchainTXID: "0xffffeeee",
-			ProtocolID:     "tx1",
-			Info:           info,
-		},
-	}
-
-	mdi.On("GetTokenPoolByLocator", em.ctx, "ns1", "erc1155", "123").Return(nil, nil).Times(2)
-	mdi.On("GetOperations", em.ctx, "ns1", mock.Anything).Return(nil, nil, fmt.Errorf("pop")).Once()
-	mdi.On("GetOperations", em.ctx, "ns1", mock.Anything).Return(operations, nil, nil).Once()
-
-	err := em.TokenPoolCreated(mti, pool)
-	assert.NoError(t, err)
-
-	mti.AssertExpectations(t)
-	mdi.AssertExpectations(t)
-}
-
 func TestTokenPoolCreatedAnnounceBadOpInputID(t *testing.T) {
 	em, cancel := newTestEventManager(t)
 	defer cancel()
diff --git a/internal/events/tokens_approved_test.go b/internal/events/tokens_approved_test.go
index 641e5fdbf..36bd12dea 100644
--- a/internal/events/tokens_approved_test.go
+++ b/internal/events/tokens_approved_test.go
@@ -123,27 +123,6 @@ func TestPersistApprovalDuplicate(t *testing.T) {
 	mdi.AssertExpectations(t)
 }
 
-func TestPersistApprovalWrongNS(t *testing.T) {
-	em, cancel := newTestEventManager(t)
-	defer cancel()
-
-	mdi := em.database.(*databasemocks.Plugin)
-
-	approval := newApproval()
-	pool := &core.TokenPool{
-		ID:        fftypes.NewUUID(),
-		Namespace: "ns2",
-	}
-
-	mdi.On("GetTokenPoolByLocator", em.ctx, "ns1", "erc1155", "F1").Return(pool, nil)
-
-	valid, err := em.persistTokenApproval(em.ctx, approval)
-	assert.False(t, valid)
-	assert.NoError(t, err)
-
-	mdi.AssertExpectations(t)
-}
-
 func TestPersistApprovalOpFail(t *testing.T) {
 	em, cancel := newTestEventManager(t)
 	defer cancel()
diff --git a/internal/events/tokens_transferred_test.go b/internal/events/tokens_transferred_test.go
index f2f817f94..61e9afe3c 100644
--- a/internal/events/tokens_transferred_test.go
+++ b/internal/events/tokens_transferred_test.go
@@ -123,27 +123,6 @@ func TestTokensTransferredIgnoreExisting(t *testing.T) {
 	mti.AssertExpectations(t)
 }
 
-func TestPersistTransferWrongNS(t *testing.T) {
-	em, cancel := newTestEventManager(t)
-	defer cancel()
-
-	mdi := em.database.(*databasemocks.Plugin)
-
-	transfer := newTransfer()
-	pool := &core.TokenPool{
-		ID:        fftypes.NewUUID(),
-		Namespace: "ns2",
-	}
-
-	mdi.On("GetTokenPoolByLocator", em.ctx, "ns1", "erc1155", "F1").Return(pool, nil)
-
-	valid, err := em.persistTokenTransfer(em.ctx, transfer)
-	assert.False(t, valid)
-	assert.NoError(t, err)
-
-	mdi.AssertExpectations(t)
-}
-
 func TestPersistTransferOpFail(t *testing.T) {
 	em, cancel := newTestEventManager(t)
 	defer cancel()
diff --git a/internal/multiparty/manager_test.go b/internal/multiparty/manager_test.go
index 3535d892a..942f4168a 100644
--- a/internal/multiparty/manager_test.go
+++ b/internal/multiparty/manager_test.go
@@ -58,7 +58,7 @@ func newTestMultipartyManager() *testMultipartyManager {
 		mth: &txcommonmocks.Helper{},
 		multipartyManager: multipartyManager{
 			ctx:       context.Background(),
-			namespace: "ns1",
+			namespace: core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"},
 		},
 	}
 
@@ -80,7 +80,8 @@ func TestNewMultipartyManager(t *testing.T) {
 	mom.On("RegisterHandler", mock.Anything, mock.Anything, []core.OpType{
 		core.OpTypeBlockchainPinBatch,
 	}).Return()
-	nm, err := NewMultipartyManager(context.Background(), "namespace", config, mdi, mbi, mom, mmi, mth)
+	ns := core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"}
+	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())
@@ -89,7 +90,7 @@ func TestNewMultipartyManager(t *testing.T) {
 
 func TestInitFail(t *testing.T) {
 	config := Config{Contracts: []Contract{}}
-	_, err := NewMultipartyManager(context.Background(), "namespace", config, nil, nil, nil, nil, nil)
+	_, err := NewMultipartyManager(context.Background(), core.NamespaceRef{}, config, nil, nil, nil, nil, nil)
 	assert.Regexp(t, "FF10128", err)
 }
 
@@ -132,6 +133,7 @@ func TestConfigureContractOldestBlock(t *testing.T) {
 	contracts[0] = contract
 	mp := newTestMultipartyManager()
 	defer mp.cleanup(t)
+	mp.namespace.LocalName = core.LegacySystemNamespace
 
 	mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
 	mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
@@ -301,8 +303,6 @@ func TestSubmitNetworkAction(t *testing.T) {
 		Active: core.FireFlyContractInfo{Index: 0},
 	}
 
-	mp.namespace = core.LegacySystemNamespace
-
 	err := mp.ConfigureContract(context.Background(), cf)
 	assert.NoError(t, err)
 	err = mp.SubmitNetworkAction(context.Background(), "0x123", &core.NetworkAction{Type: core.NetworkActionTerminate})
@@ -337,8 +337,6 @@ func TestSubmitNetworkActionTXFail(t *testing.T) {
 		Active: core.FireFlyContractInfo{Index: 0},
 	}
 
-	mp.namespace = core.LegacySystemNamespace
-
 	err := mp.ConfigureContract(context.Background(), cf)
 	assert.NoError(t, err)
 	err = mp.SubmitNetworkAction(context.Background(), "0x123", &core.NetworkAction{Type: core.NetworkActionTerminate})
@@ -375,8 +373,6 @@ func TestSubmitNetworkActionOpFail(t *testing.T) {
 		Active: core.FireFlyContractInfo{Index: 0},
 	}
 
-	mp.namespace = core.LegacySystemNamespace
-
 	err := mp.ConfigureContract(context.Background(), cf)
 	assert.NoError(t, err)
 	err = mp.SubmitNetworkAction(context.Background(), "0x123", &core.NetworkAction{Type: core.NetworkActionTerminate})
@@ -409,8 +405,6 @@ func TestSubmitNetworkActionBadType(t *testing.T) {
 		Active: core.FireFlyContractInfo{Index: 0},
 	}
 
-	mp.namespace = core.LegacySystemNamespace
-
 	err := mp.ConfigureContract(context.Background(), cf)
 	assert.NoError(t, err)
 	err = mp.SubmitNetworkAction(context.Background(), "0x123", &core.NetworkAction{Type: "BAD"})
diff --git a/internal/namespace/manager_test.go b/internal/namespace/manager_test.go
index 49f58adc6..c4d757169 100644
--- a/internal/namespace/manager_test.go
+++ b/internal/namespace/manager_test.go
@@ -291,6 +291,7 @@ func TestInitOrchestratorFail(t *testing.T) {
 	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.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()
diff --git a/internal/orchestrator/bound_callbacks_test.go b/internal/orchestrator/bound_callbacks_test.go
index 468b4d08c..fddc14db2 100644
--- a/internal/orchestrator/bound_callbacks_test.go
+++ b/internal/orchestrator/bound_callbacks_test.go
@@ -72,9 +72,7 @@ func TestBoundCallbacks(t *testing.T) {
 		Output:         info,
 	}).Return()
 
-	bc.BlockchainOpUpdate(mbi, nsOpID, core.OpStatusFailed, "0xffffeeee", "error info", info)
-
-	bc.TokenOpUpdate(mti, nsOpID, core.OpStatusFailed, "0xffffeeee", "error info", info)
+	bc.OperationUpdate(mbi, nsOpID, core.OpStatusFailed, "0xffffeeee", "error info", info)
 
 	mde := &dataexchangemocks.DXEvent{}
 	mom.On("TransferResult", mdx, mde).Return()
diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go
index aa48d5e66..b2c2218c3 100644
--- a/internal/orchestrator/orchestrator_test.go
+++ b/internal/orchestrator/orchestrator_test.go
@@ -114,7 +114,7 @@ func newTestOrchestrator() *testOrchestrator {
 		orchestrator: orchestrator{
 			ctx:       ctx,
 			cancelCtx: cancel,
-			namespace: "ns",
+			namespace: core.NamespaceRef{LocalName: "ns", RemoteName: "ns"},
 		},
 		mdi: &databasemocks.Plugin{},
 		mdm: &datamocks.Manager{},
@@ -182,7 +182,7 @@ func newTestOrchestrator() *testOrchestrator {
 
 func TestNewOrchestrator(t *testing.T) {
 	or := NewOrchestrator(
-		"ns1",
+		core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"},
 		Config{},
 		Plugins{},
 		&metricsmocks.Manager{},
@@ -193,13 +193,16 @@ func TestNewOrchestrator(t *testing.T) {
 func TestInitOK(t *testing.T) {
 	or := newTestOrchestrator()
 	defer or.cleanup(t)
+	or.namespace.RemoteName = "ns2"
 	or.mdi.On("SetHandler", "ns", mock.Anything).Return()
-	or.mbi.On("SetHandler", "ns", mock.Anything).Return()
+	or.mbi.On("SetHandler", "ns2", mock.Anything).Return()
+	or.mbi.On("SetOperationHandler", "ns", mock.Anything).Return()
 	or.mdi.On("GetIdentities", mock.Anything, "ns", mock.Anything).Return([]*core.Identity{{}}, nil, nil)
-	or.mdx.On("SetHandler", "ns", mock.Anything).Return()
+	or.mdx.On("SetHandler", "ns2", mock.Anything).Return()
 	or.mdx.On("SetNodes", mock.Anything).Return()
-	or.mps.On("SetHandler", "ns", mock.Anything).Return()
+	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.cancelCtx)
 	assert.NoError(t, err)
 
@@ -221,6 +224,7 @@ func TestInitTokenListenerFail(t *testing.T) {
 	defer or.cleanup(t)
 	or.mdi.On("SetHandler", "ns", mock.Anything).Return()
 	or.mbi.On("SetHandler", "ns", mock.Anything).Return()
+	or.mbi.On("SetOperationHandler", "ns", mock.Anything).Return()
 	or.mdi.On("GetIdentities", mock.Anything, "ns", mock.Anything).Return([]*core.Identity{{}}, nil, nil)
 	or.mdx.On("SetHandler", "ns", mock.Anything).Return()
 	or.mdx.On("SetNodes", mock.Anything).Return()
@@ -235,6 +239,7 @@ func TestInitDataexchangeNodesFail(t *testing.T) {
 	defer or.cleanup(t)
 	or.mdi.On("SetHandler", "ns", mock.Anything).Return()
 	or.mbi.On("SetHandler", "ns", mock.Anything).Return()
+	or.mbi.On("SetOperationHandler", "ns", mock.Anything).Return()
 	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()
@@ -407,7 +412,7 @@ func TestStartStopOk(t *testing.T) {
 
 func TestNetworkAction(t *testing.T) {
 	or := newTestOrchestrator()
-	or.namespace = core.LegacySystemNamespace
+	or.namespace.LocalName = core.LegacySystemNamespace
 	action := &core.NetworkAction{Type: core.NetworkActionTerminate}
 	or.mim.On("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x123", nil)
 	or.mmp.On("SubmitNetworkAction", context.Background(), "0x123", action).Return(nil)
@@ -417,7 +422,7 @@ func TestNetworkAction(t *testing.T) {
 
 func TestNetworkActionBadKey(t *testing.T) {
 	or := newTestOrchestrator()
-	or.namespace = core.LegacySystemNamespace
+	or.namespace.LocalName = core.LegacySystemNamespace
 	action := &core.NetworkAction{Type: core.NetworkActionTerminate}
 	or.mim.On("NormalizeSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("", fmt.Errorf("pop"))
 	err := or.SubmitNetworkAction(context.Background(), action)
diff --git a/internal/orchestrator/persistence_events_test.go b/internal/orchestrator/persistence_events_test.go
index 6b34d743c..d92ea7359 100644
--- a/internal/orchestrator/persistence_events_test.go
+++ b/internal/orchestrator/persistence_events_test.go
@@ -30,7 +30,7 @@ import (
 func TestMessageCreated(t *testing.T) {
 	mb := &batchmocks.Manager{}
 	o := &orchestrator{
-		namespace: "ns1",
+		namespace: core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"},
 		batch:     mb,
 	}
 	mb.On("NewMessages").Return((chan<- int64)(make(chan int64, 1)))
@@ -41,7 +41,7 @@ func TestMessageCreated(t *testing.T) {
 func TestPinCreated(t *testing.T) {
 	mem := &eventmocks.EventManager{}
 	o := &orchestrator{
-		namespace: "ns1",
+		namespace: core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"},
 		events:    mem,
 	}
 	mem.On("NewPins").Return((chan<- int64)(make(chan int64, 1)))
@@ -52,7 +52,7 @@ func TestPinCreated(t *testing.T) {
 func TestEventCreated(t *testing.T) {
 	mem := &eventmocks.EventManager{}
 	o := &orchestrator{
-		namespace: "ns1",
+		namespace: core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"},
 		events:    mem,
 	}
 	mem.On("NewEvents").Return((chan<- int64)(make(chan int64, 1)))
@@ -63,7 +63,7 @@ func TestEventCreated(t *testing.T) {
 func TestSubscriptionCreated(t *testing.T) {
 	mem := &eventmocks.EventManager{}
 	o := &orchestrator{
-		namespace: "ns1",
+		namespace: core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"},
 		events:    mem,
 	}
 	mem.On("NewSubscriptions").Return((chan<- *fftypes.UUID)(make(chan *fftypes.UUID, 1)))
@@ -74,7 +74,7 @@ func TestSubscriptionCreated(t *testing.T) {
 func TestSubscriptionUpdated(t *testing.T) {
 	mem := &eventmocks.EventManager{}
 	o := &orchestrator{
-		namespace: "ns1",
+		namespace: core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"},
 		events:    mem,
 	}
 	mem.On("SubscriptionUpdates").Return((chan<- *fftypes.UUID)(make(chan *fftypes.UUID, 1)))
@@ -85,7 +85,7 @@ func TestSubscriptionUpdated(t *testing.T) {
 func TestSubscriptionDeleted(t *testing.T) {
 	mem := &eventmocks.EventManager{}
 	o := &orchestrator{
-		namespace: "ns1",
+		namespace: core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"},
 		events:    mem,
 	}
 	mem.On("DeletedSubscriptions").Return((chan<- *fftypes.UUID)(make(chan *fftypes.UUID, 1)))
@@ -96,7 +96,7 @@ func TestSubscriptionDeleted(t *testing.T) {
 func TestOrderedUUIDCollectionWrongNS(t *testing.T) {
 	o := &orchestrator{
 		ctx:       context.Background(),
-		namespace: "ns1",
+		namespace: core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"},
 	}
 	o.OrderedUUIDCollectionNSEvent(database.CollectionMessages, core.ChangeEventTypeCreated, "ns2", fftypes.NewUUID(), 1)
 }
@@ -104,7 +104,7 @@ func TestOrderedUUIDCollectionWrongNS(t *testing.T) {
 func TestOrderedCollectionWrongNS(t *testing.T) {
 	o := &orchestrator{
 		ctx:       context.Background(),
-		namespace: "ns1",
+		namespace: core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"},
 	}
 	o.OrderedCollectionNSEvent(database.CollectionPins, core.ChangeEventTypeCreated, "ns2", 1)
 }
@@ -112,7 +112,7 @@ func TestOrderedCollectionWrongNS(t *testing.T) {
 func TestUUIDCollectionWrongNS(t *testing.T) {
 	o := &orchestrator{
 		ctx:       context.Background(),
-		namespace: "ns1",
+		namespace: core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"},
 	}
 	o.UUIDCollectionNSEvent(database.CollectionSubscriptions, core.ChangeEventTypeCreated, "ns2", fftypes.NewUUID())
 }
diff --git a/internal/orchestrator/status_test.go b/internal/orchestrator/status_test.go
index 92b62d11d..8c44cc4ec 100644
--- a/internal/orchestrator/status_test.go
+++ b/internal/orchestrator/status_test.go
@@ -138,8 +138,8 @@ func TestGetStatusRegistered(t *testing.T) {
 	assert.ElementsMatch(t, pluginsResult.SharedStorage, status.Plugins.SharedStorage)
 	assert.ElementsMatch(t, pluginsResult.Tokens, status.Plugins.Tokens)
 
-	assert.True(t, or.GetNodeUUID(or.ctx, "ns").Equals(nodeID))
-	assert.True(t, or.GetNodeUUID(or.ctx, "ns").Equals(nodeID)) // cached
+	assert.True(t, or.GetNodeUUID(or.ctx).Equals(nodeID))
+	assert.True(t, or.GetNodeUUID(or.ctx).Equals(nodeID)) // cached
 
 }
 
@@ -259,7 +259,7 @@ func TestGetStatusUnregistered(t *testing.T) {
 	assert.Equal(t, "node1", status.Node.Name)
 	assert.False(t, status.Node.Registered)
 
-	assert.Nil(t, or.GetNodeUUID(or.ctx, "ns"))
+	assert.Nil(t, or.GetNodeUUID(or.ctx))
 
 }
 
@@ -315,7 +315,7 @@ func TestGetStatusOrgOnlyRegistered(t *testing.T) {
 	assert.ElementsMatch(t, pluginsResult.SharedStorage, status.Plugins.SharedStorage)
 	assert.ElementsMatch(t, pluginsResult.Tokens, status.Plugins.Tokens)
 
-	assert.Nil(t, or.GetNodeUUID(or.ctx, "ns"))
+	assert.Nil(t, or.GetNodeUUID(or.ctx))
 }
 
 func TestGetStatusNodeError(t *testing.T) {
@@ -350,6 +350,6 @@ func TestGetStatusNodeError(t *testing.T) {
 	_, err := or.GetStatus(or.ctx)
 	assert.EqualError(t, err, "pop")
 
-	assert.Nil(t, or.GetNodeUUID(or.ctx, "ns"))
+	assert.Nil(t, or.GetNodeUUID(or.ctx))
 
 }
diff --git a/internal/privatemessaging/privatemessaging_test.go b/internal/privatemessaging/privatemessaging_test.go
index 6546ccc6e..9d6bfedee 100644
--- a/internal/privatemessaging/privatemessaging_test.go
+++ b/internal/privatemessaging/privatemessaging_test.go
@@ -77,7 +77,8 @@ func newTestPrivateMessagingCommon(t *testing.T, metricsEnabled bool) (*privateM
 	mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything)
 
 	ctx, cancel := context.WithCancel(context.Background())
-	pm, err := NewPrivateMessaging(ctx, "ns1", mdi, mdx, mbi, mim, mba, mdm, msa, mmp, mmi, mom)
+	ns := core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"}
+	pm, err := NewPrivateMessaging(ctx, ns, mdi, mdx, mbi, mim, mba, mdm, msa, mmp, mmi, mom)
 	assert.NoError(t, err)
 
 	// Default mocks to save boilerplate in the tests
@@ -212,7 +213,7 @@ func TestDispatchBatchWithBlobs(t *testing.T) {
 }
 
 func TestNewPrivateMessagingMissingDeps(t *testing.T) {
-	_, err := NewPrivateMessaging(context.Background(), "", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
+	_, err := NewPrivateMessaging(context.Background(), core.NamespaceRef{}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
 	assert.Regexp(t, "FF10128", err)
 }
 
diff --git a/internal/shareddownload/download_manager_test.go b/internal/shareddownload/download_manager_test.go
index 407f6a87e..42eec099c 100644
--- a/internal/shareddownload/download_manager_test.go
+++ b/internal/shareddownload/download_manager_test.go
@@ -58,14 +58,15 @@ func newTestDownloadManager(t *testing.T) (*downloadManager, func()) {
 	assert.NoError(t, err)
 
 	ctx, cancel := context.WithCancel(context.Background())
-	pm, err := NewDownloadManager(ctx, "ns1", mdi, mss, mdx, operations, mci)
+	ns := core.NamespaceRef{LocalName: "ns1", RemoteName: "ns1"}
+	pm, err := NewDownloadManager(ctx, ns, mdi, mss, mdx, operations, mci)
 	assert.NoError(t, err)
 
 	return pm.(*downloadManager), cancel
 }
 
 func TestNewDownloadManagerMissingDeps(t *testing.T) {
-	_, err := NewDownloadManager(context.Background(), "", nil, nil, nil, nil, nil)
+	_, err := NewDownloadManager(context.Background(), core.NamespaceRef{}, nil, nil, nil, nil, nil)
 	assert.Regexp(t, "FF10128", err)
 }
 
diff --git a/internal/tokens/fftokens/fftokens_test.go b/internal/tokens/fftokens/fftokens_test.go
index 31b6b1290..7190c0d8f 100644
--- a/internal/tokens/fftokens/fftokens_test.go
+++ b/internal/tokens/fftokens/fftokens_test.go
@@ -31,6 +31,7 @@ import (
 	"github.com/hyperledger/firefly-common/pkg/fftypes"
 	"github.com/hyperledger/firefly-common/pkg/wsclient"
 	"github.com/hyperledger/firefly/internal/coreconfig"
+	"github.com/hyperledger/firefly/mocks/coremocks"
 	"github.com/hyperledger/firefly/mocks/tokenmocks"
 	"github.com/hyperledger/firefly/mocks/wsmocks"
 	"github.com/hyperledger/firefly/pkg/core"
@@ -829,20 +830,20 @@ func TestReceiptEvents(t *testing.T) {
 	err := h.Start()
 	assert.NoError(t, err)
 
-	mcb := &tokenmocks.Callbacks{}
-	h.SetHandler("ns1", mcb)
+	mcb := &coremocks.OperationCallbacks{}
+	h.SetOperationHandler("ns1", mcb)
 	opID := fftypes.NewUUID()
 
 	// receipt: bad ID - passed through
-	mcb.On("TokenOpUpdate", h, "wrong", core.OpStatusFailed, "", "", mock.Anything).Return(nil).Once()
+	mcb.On("OperationUpdate", h, "wrong", core.OpStatusFailed, "", "", mock.Anything).Return(nil).Once()
 	fromServer <- fftypes.JSONObject{
 		"id":    "3",
 		"event": "receipt",
-		"data":  fftypes.JSONObject{"id": "wrong"}, // passed through to TokenOpUpdate to ignore
+		"data":  fftypes.JSONObject{"id": "wrong"}, // passed through to OperationUpdate to ignore
 	}.String()
 
 	// receipt: success
-	mcb.On("TokenOpUpdate", h, "ns1:"+opID.String(), core.OpStatusSucceeded, "0xffffeeee", "", mock.Anything).Return(nil).Once()
+	mcb.On("OperationUpdate", h, "ns1:"+opID.String(), core.OpStatusSucceeded, "0xffffeeee", "", mock.Anything).Return(nil).Once()
 	fromServer <- fftypes.JSONObject{
 		"id":    "4",
 		"event": "receipt",
@@ -854,7 +855,7 @@ func TestReceiptEvents(t *testing.T) {
 	}.String()
 
 	// receipt: failure
-	mcb.On("TokenOpUpdate", h, "ns1:"+opID.String(), core.OpStatusFailed, "0xffffeeee", "", mock.Anything).Return(nil).Once()
+	mcb.On("OperationUpdate", h, "ns1:"+opID.String(), core.OpStatusFailed, "0xffffeeee", "", mock.Anything).Return(nil).Once()
 	fromServer <- fftypes.JSONObject{
 		"id":    "5",
 		"event": "receipt",
@@ -1316,7 +1317,7 @@ func TestCallbacksWrongNamespace(t *testing.T) {
 	h, _, _, _, done := newTestFFTokens(t)
 	defer done()
 	nsOpID := "ns1:" + fftypes.NewUUID().String()
-	h.callbacks.TokenOpUpdate(context.Background(), h, nsOpID, core.OpStatusSucceeded, "tx123", "", nil)
+	h.callbacks.OperationUpdate(context.Background(), h, nsOpID, core.OpStatusSucceeded, "tx123", "", nil)
 	h.callbacks.TokenPoolCreated(context.Background(), "ns1", h, nil)
 	h.callbacks.TokensTransferred(context.Background(), "ns1", h, nil)
 	h.callbacks.TokensApproved(context.Background(), "ns1", h, nil)
diff --git a/pkg/core/datatype_test.go b/pkg/core/datatype_test.go
index dc463356c..790f3239b 100644
--- a/pkg/core/datatype_test.go
+++ b/pkg/core/datatype_test.go
@@ -31,12 +31,6 @@ func TestDatatypeValidation(t *testing.T) {
 	}
 	assert.Regexp(t, "FF00111.*wrong", dt.Validate(context.Background(), false))
 
-	dt = &Datatype{
-		Validator: ValidatorTypeJSON,
-		Namespace: "!wrong",
-	}
-	assert.Regexp(t, "FF00140.*namespace", dt.Validate(context.Background(), false))
-
 	dt = &Datatype{
 		Validator: ValidatorTypeJSON,
 		Namespace: "ok",
diff --git a/pkg/core/identity_test.go b/pkg/core/identity_test.go
index 94839c4b7..e54d2c1c9 100644
--- a/pkg/core/identity_test.go
+++ b/pkg/core/identity_test.go
@@ -93,10 +93,6 @@ func TestIdentityValidationOrgs(t *testing.T) {
 	o.ID = nil
 	assert.Regexp(t, "FF00114", o.Validate(ctx))
 
-	o = testOrg()
-	o.Namespace = "!namespace"
-	assert.Regexp(t, "FF00140", o.Validate(ctx))
-
 	o = testOrg()
 	o.Name = "!name"
 	assert.Regexp(t, "FF00140", o.Validate(ctx))
diff --git a/pkg/core/tokenpool_test.go b/pkg/core/tokenpool_test.go
index e0a30d334..7acd506f1 100644
--- a/pkg/core/tokenpool_test.go
+++ b/pkg/core/tokenpool_test.go
@@ -26,17 +26,10 @@ import (
 
 func TestTokenPoolValidation(t *testing.T) {
 	pool := &TokenPool{
-		Namespace: "!wrong",
-		Name:      "ok",
-	}
-	err := pool.Validate(context.Background())
-	assert.Regexp(t, "FF00140.*'namespace'", err)
-
-	pool = &TokenPool{
 		Namespace: "ok",
 		Name:      "!wrong",
 	}
-	err = pool.Validate(context.Background())
+	err := pool.Validate(context.Background())
 	assert.Regexp(t, "FF00140.*'name'", err)
 
 	pool = &TokenPool{