Repository: hyperledger/firefly
Pull request #1163 (2023-01-26 23:53:50)

Add "txid" and "txparent" to messages
Propagate the `batch_pin` transaction ID onto the message when the batch is assembled (sender) or when it is persisted (receiver), as a new `txid` field.

For token transfers and approvals that come with a message, store the `token_transfer` or `token_approval` transaction details on the message up-front, as a new `txparent` field.

Illustration of changes to the message object:
<img width="912" alt="message with transfer" src="https://user-images.githubusercontent.com/1993829/215154143-3199d9d1-4f12-4bdc-a0d8-5da14f84caaa.png">

This does mean the allocation of the `token_transfer` or `token_approval` transaction ID happens earlier and outside a database transaction, but I don't see a problem with this.

This probably supersedes both #1157 and #1162, as I think it's a cleaner route toward solving that problem _and_ teeing up [FIR-17](https://github.com/hyperledger/firefly-fir/pull/17).

codecov-commenter: # [Codecov](https://codecov.io/gh/hyperledger/firefly/pull/1163?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) Report
> Merging [#1163](https://codecov.io/gh/hyperledger/firefly/pull/1163?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) (76288b5) into [main](https://codecov.io/gh/hyperledger/firefly/commit/15d5c76008a474b88b7646e1b6d416a011175f3d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) (15d5c76) will **decrease** coverage by `0.05%`.
> The diff coverage is `84.61%`.

```diff
@@            Coverage Diff             @@
##             main    #1163      +/-   ##
==========================================
- Coverage   99.98%   99.94%   -0.05%     
==========================================
  Files         305      305              
  Lines       19964    20007      +43     
==========================================
+ Hits        19962    19995      +33     
- Misses          2        8       +6     
- Partials        0        4       +4     
```


| [Impacted Files](https://codecov.io/gh/hyperledger/firefly/pull/1163?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger) | Coverage Δ | |
|---|---|---|
| [internal/database/sqlcommon/message\_sql.go](https://codecov.io/gh/hyperledger/firefly/pull/1163?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvZGF0YWJhc2Uvc3FsY29tbW9uL21lc3NhZ2Vfc3FsLmdv) | `98.00% <63.63%> (-2.00%)` | :arrow_down: |
| [internal/assets/token\_approval.go](https://codecov.io/gh/hyperledger/firefly/pull/1163?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYXNzZXRzL3Rva2VuX2FwcHJvdmFsLmdv) | `100.00% <100.00%> (ø)` | |
| [internal/assets/token\_transfer.go](https://codecov.io/gh/hyperledger/firefly/pull/1163?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYXNzZXRzL3Rva2VuX3RyYW5zZmVyLmdv) | `100.00% <100.00%> (ø)` | |
| [internal/batch/batch\_processor.go](https://codecov.io/gh/hyperledger/firefly/pull/1163?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYmF0Y2gvYmF0Y2hfcHJvY2Vzc29yLmdv) | `100.00% <100.00%> (ø)` | |
| [internal/events/aggregator.go](https://codecov.io/gh/hyperledger/firefly/pull/1163?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvZXZlbnRzL2FnZ3JlZ2F0b3IuZ28=) | `100.00% <100.00%> (ø)` | |
| [internal/events/persist\_batch.go](https://codecov.io/gh/hyperledger/firefly/pull/1163?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvZXZlbnRzL3BlcnNpc3RfYmF0Y2guZ28=) | `100.00% <100.00%> (ø)` | |
| [pkg/core/message.go](https://codecov.io/gh/hyperledger/firefly/pull/1163?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-cGtnL2NvcmUvbWVzc2FnZS5nbw==) | `100.00% <100.00%> (ø)` | |
| [internal/apiserver/server.go](https://codecov.io/gh/hyperledger/firefly/pull/1163?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger#diff-aW50ZXJuYWwvYXBpc2VydmVyL3NlcnZlci5nbw==) | `99.08% <0.00%> (-0.92%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperledger)


Patch:
diff --git a/db/migrations/postgres/000106_add_message_transactions.down.sql b/db/migrations/postgres/000106_add_message_transactions.down.sql
new file mode 100644
index 000000000..fbbdef358
--- /dev/null
+++ b/db/migrations/postgres/000106_add_message_transactions.down.sql
@@ -0,0 +1,5 @@
+BEGIN;
+ALTER TABLE messages DROP COLUMN tx_id;
+ALTER TABLE messages DROP COLUMN tx_parent_type;
+ALTER TABLE messages DROP COLUMN tx_parent_id;
+COMMIT;
diff --git a/db/migrations/postgres/000106_add_message_transactions.up.sql b/db/migrations/postgres/000106_add_message_transactions.up.sql
new file mode 100644
index 000000000..bd46dccf4
--- /dev/null
+++ b/db/migrations/postgres/000106_add_message_transactions.up.sql
@@ -0,0 +1,7 @@
+BEGIN;
+ALTER TABLE messages ADD COLUMN tx_id UUID;
+UPDATE messages SET tx_id = batches.tx_id
+  FROM batches WHERE messages.batch_id = batches.id AND messages.tx_id IS NULL;
+ALTER TABLE messages ADD COLUMN tx_parent_type VARCHAR(64);
+ALTER TABLE messages ADD COLUMN tx_parent_id UUID;
+COMMIT;
diff --git a/db/migrations/sqlite/000106_add_message_transactions.down.sql b/db/migrations/sqlite/000106_add_message_transactions.down.sql
new file mode 100644
index 000000000..6a803891a
--- /dev/null
+++ b/db/migrations/sqlite/000106_add_message_transactions.down.sql
@@ -0,0 +1,3 @@
+ALTER TABLE messages DROP COLUMN tx_id;
+ALTER TABLE messages DROP COLUMN tx_parent_type;
+ALTER TABLE messages DROP COLUMN tx_parent_id;
diff --git a/db/migrations/sqlite/000106_add_message_transactions.up.sql b/db/migrations/sqlite/000106_add_message_transactions.up.sql
new file mode 100644
index 000000000..e065768d4
--- /dev/null
+++ b/db/migrations/sqlite/000106_add_message_transactions.up.sql
@@ -0,0 +1,5 @@
+ALTER TABLE messages ADD COLUMN tx_id UUID;
+UPDATE messages SET tx_id = batches.tx_id
+  FROM batches WHERE messages.batch_id = batches.id AND messages.tx_id IS NULL;
+ALTER TABLE messages ADD COLUMN tx_parent_type VARCHAR(64);
+ALTER TABLE messages ADD COLUMN tx_parent_id UUID;
diff --git a/docs/reference/types/message.md b/docs/reference/types/message.md
index c64a3b3cf..271fbb5ce 100644
--- a/docs/reference/types/message.md
+++ b/docs/reference/types/message.md
@@ -60,6 +60,7 @@ nav_order: 15
 | `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) |
+| `txid` | The ID of the transaction used to order/deliver this message | [`UUID`](simpletypes#uuid) |
 | `state` | The current state of the message | `FFEnum`:<br/>`"staged"`<br/>`"ready"`<br/>`"sent"`<br/>`"pending"`<br/>`"confirmed"`<br/>`"rejected"` |
 | `confirmed` | The timestamp of when the message was confirmed/rejected | [`FFTime`](simpletypes#fftime) |
 | `data` | The list of data elements attached to the message | [`DataRef[]`](#dataref) |
@@ -81,6 +82,15 @@ nav_order: 15
 | `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` |
+| `txparent` | The parent transaction that originally triggered this message | [`TransactionRef`](#transactionref) |
+
+## TransactionRef
+
+| Field Name | Description | Type |
+|------------|-------------|------|
+| `type` | The type of the FireFly transaction | `FFEnum`: |
+| `id` | The UUID of the FireFly transaction | [`UUID`](simpletypes#uuid) |
+
 
 
 ## DataRef
diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml
index 44fbd5f34..75050966e 100644
--- a/docs/swagger/swagger.yaml
+++ b/docs/swagger/swagger.yaml
@@ -5270,6 +5270,21 @@ paths:
         name: topics
         schema:
           type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txid
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.id
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.type
+        schema:
+          type: string
       - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
         in: query
         name: txtype
@@ -5527,6 +5542,21 @@ paths:
         name: topics
         schema:
           type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txid
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.id
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.type
+        schema:
+          type: string
       - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
         in: query
         name: txtype
@@ -5658,6 +5688,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -5713,6 +5755,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         default:
@@ -5818,6 +5865,21 @@ paths:
         name: topics
         schema:
           type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txid
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.id
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.type
+        schema:
+          type: string
       - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
         in: query
         name: txtype
@@ -7964,6 +8026,21 @@ paths:
         name: topics
         schema:
           type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txid
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.id
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.type
+        schema:
+          type: string
       - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
         in: query
         name: txtype
@@ -8097,6 +8174,18 @@ paths:
                               assigned - using the default topic is discouraged
                             type: string
                           type: array
+                        txparent:
+                          description: The parent transaction that originally triggered
+                            this message
+                          properties:
+                            id:
+                              description: The UUID of the FireFly transaction
+                              format: uuid
+                              type: string
+                            type:
+                              description: The type of the FireFly transaction
+                              type: string
+                          type: object
                         txtype:
                           description: The type of transaction used to order/deliver
                             this message
@@ -8152,6 +8241,11 @@ paths:
                       - confirmed
                       - rejected
                       type: string
+                    txid:
+                      description: The ID of the transaction used to order/deliver
+                        this message
+                      format: uuid
+                      type: string
                   type: object
                 type: array
           description: Success
@@ -8349,6 +8443,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -8404,6 +8510,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         default:
@@ -8960,6 +9071,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -9015,6 +9138,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         "202":
@@ -9097,6 +9225,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -9152,6 +9292,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         default:
@@ -9404,6 +9549,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -9459,6 +9616,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         "202":
@@ -9547,6 +9709,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -9602,6 +9776,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         default:
@@ -9930,6 +10109,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -9985,6 +10176,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         default:
@@ -15811,6 +16007,21 @@ paths:
         name: topics
         schema:
           type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txid
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.id
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.type
+        schema:
+          type: string
       - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
         in: query
         name: txtype
@@ -16082,6 +16293,21 @@ paths:
         name: topics
         schema:
           type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txid
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.id
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.type
+        schema:
+          type: string
       - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
         in: query
         name: txtype
@@ -16213,6 +16439,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -16268,6 +16506,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         default:
@@ -16380,6 +16623,21 @@ paths:
         name: topics
         schema:
           type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txid
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.id
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.type
+        schema:
+          type: string
       - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
         in: query
         name: txtype
@@ -18638,6 +18896,21 @@ paths:
         name: topics
         schema:
           type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txid
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.id
+        schema:
+          type: string
+      - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
+        in: query
+        name: txparent.type
+        schema:
+          type: string
       - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
         in: query
         name: txtype
@@ -18771,6 +19044,18 @@ paths:
                               assigned - using the default topic is discouraged
                             type: string
                           type: array
+                        txparent:
+                          description: The parent transaction that originally triggered
+                            this message
+                          properties:
+                            id:
+                              description: The UUID of the FireFly transaction
+                              format: uuid
+                              type: string
+                            type:
+                              description: The type of the FireFly transaction
+                              type: string
+                          type: object
                         txtype:
                           description: The type of transaction used to order/deliver
                             this message
@@ -18826,6 +19111,11 @@ paths:
                       - confirmed
                       - rejected
                       type: string
+                    txid:
+                      description: The ID of the transaction used to order/deliver
+                        this message
+                      format: uuid
+                      type: string
                   type: object
                 type: array
           description: Success
@@ -19030,6 +19320,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -19085,6 +19387,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         default:
@@ -19713,6 +20020,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -19768,6 +20087,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         "202":
@@ -19856,6 +20180,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -19911,6 +20247,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         default:
@@ -20170,6 +20511,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -20225,6 +20578,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         "202":
@@ -20313,6 +20671,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -20368,6 +20738,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         default:
@@ -20703,6 +21078,18 @@ paths:
                             - using the default topic is discouraged
                           type: string
                         type: array
+                      txparent:
+                        description: The parent transaction that originally triggered
+                          this message
+                        properties:
+                          id:
+                            description: The UUID of the FireFly transaction
+                            format: uuid
+                            type: string
+                          type:
+                            description: The type of the FireFly transaction
+                            type: string
+                        type: object
                       txtype:
                         description: The type of transaction used to order/deliver
                           this message
@@ -20758,6 +21145,11 @@ paths:
                     - confirmed
                     - rejected
                     type: string
+                  txid:
+                    description: The ID of the transaction used to order/deliver this
+                      message
+                    format: uuid
+                    type: string
                 type: object
           description: Success
         default:
diff --git a/internal/assets/token_approval.go b/internal/assets/token_approval.go
index 435e630e1..be8a796bf 100644
--- a/internal/assets/token_approval.go
+++ b/internal/assets/token_approval.go
@@ -95,8 +95,20 @@ func (s *approveSender) resolveAndSend(ctx context.Context, method sendMethod) (
 }
 
 func (s *approveSender) resolve(ctx context.Context) (err error) {
+	// Create a transaction and attach to the approval
+	txid, err := s.mgr.txHelper.SubmitNewTransaction(ctx, core.TransactionTypeTokenApproval, s.approval.IdempotencyKey)
+	if err != nil {
+		return err
+	}
+	s.approval.TX.ID = txid
+	s.approval.TX.Type = core.TransactionTypeTokenApproval
+
 	// Resolve the attached message
 	if s.approval.Message != nil {
+		s.approval.Message.Header.TxParent = &core.TransactionRef{
+			ID:   txid,
+			Type: core.TransactionTypeTokenApproval,
+		}
 		s.msgSender, err = s.buildApprovalMessage(ctx, s.approval.Message)
 		if err != nil {
 			return err
@@ -136,17 +148,10 @@ func (s *approveSender) sendInternal(ctx context.Context, method sendMethod) (er
 			return nil
 		}
 
-		txid, err := s.mgr.txHelper.SubmitNewTransaction(ctx, core.TransactionTypeTokenApproval, s.approval.IdempotencyKey)
-		if err != nil {
-			return err
-		}
-		s.approval.TX.ID = txid
-		s.approval.TX.Type = core.TransactionTypeTokenApproval
-
 		op = core.NewOperation(
 			plugin,
 			s.mgr.namespace,
-			txid,
+			s.approval.TX.ID,
 			core.TransactionTypeTokenApproval)
 		if err = txcommon.AddTokenApprovalInputs(op, &s.approval.TokenApproval); err == nil {
 			err = s.mgr.operations.AddOrReuseOperation(ctx, op)
@@ -195,19 +200,21 @@ func (am *assetManager) validateApproval(ctx context.Context, approval *core.Tok
 
 func (s *approveSender) buildApprovalMessage(ctx context.Context, in *core.MessageInOut) (syncasync.Sender, error) {
 	allowedTypes := []fftypes.FFEnum{
-		core.MessageTypeApprovalBroadcast,
-		core.MessageTypeApprovalPrivate,
+		core.MessageTypeBroadcast,
+		core.MessageTypePrivate,
+		core.MessageTypeDeprecatedApprovalBroadcast,
+		core.MessageTypeDeprecatedApprovalPrivate,
 	}
 	if in.Header.Type == "" {
-		in.Header.Type = core.MessageTypeApprovalBroadcast
+		in.Header.Type = core.MessageTypeBroadcast
 	}
 	switch in.Header.Type {
-	case core.MessageTypeApprovalBroadcast:
+	case core.MessageTypeBroadcast, core.MessageTypeDeprecatedApprovalBroadcast:
 		if s.mgr.broadcast == nil {
 			return nil, i18n.NewError(ctx, coremsgs.MsgMessagesNotSupported)
 		}
 		return s.mgr.broadcast.NewBroadcast(in), nil
-	case core.MessageTypeApprovalPrivate:
+	case core.MessageTypePrivate, core.MessageTypeDeprecatedApprovalPrivate:
 		if s.mgr.messaging == nil {
 			return nil, i18n.NewError(ctx, coremsgs.MsgMessagesNotSupported)
 		}
diff --git a/internal/assets/token_transfer.go b/internal/assets/token_transfer.go
index 86cc2e7a6..1c3ef1663 100644
--- a/internal/assets/token_transfer.go
+++ b/internal/assets/token_transfer.go
@@ -181,8 +181,20 @@ func (s *transferSender) resolveAndSend(ctx context.Context, method sendMethod)
 }
 
 func (s *transferSender) resolve(ctx context.Context) (err error) {
+	// Create a transaction and attach to the transfer
+	txid, err := s.mgr.txHelper.SubmitNewTransaction(ctx, core.TransactionTypeTokenTransfer, s.transfer.IdempotencyKey)
+	if err != nil {
+		return err
+	}
+	s.transfer.TX.ID = txid
+	s.transfer.TX.Type = core.TransactionTypeTokenTransfer
+
 	// Resolve the attached message
 	if s.transfer.Message != nil {
+		s.transfer.Message.Header.TxParent = &core.TransactionRef{
+			ID:   txid,
+			Type: core.TransactionTypeTokenTransfer,
+		}
 		s.msgSender, err = s.buildTransferMessage(ctx, s.transfer.Message)
 		if err != nil {
 			return err
@@ -225,17 +237,10 @@ func (s *transferSender) sendInternal(ctx context.Context, method sendMethod) (e
 			return nil
 		}
 
-		txid, err := s.mgr.txHelper.SubmitNewTransaction(ctx, core.TransactionTypeTokenTransfer, s.transfer.IdempotencyKey)
-		if err != nil {
-			return err
-		}
-		s.transfer.TX.ID = txid
-		s.transfer.TX.Type = core.TransactionTypeTokenTransfer
-
 		op = core.NewOperation(
 			plugin,
 			s.mgr.namespace,
-			txid,
+			s.transfer.TX.ID,
 			core.OpTypeTokenTransfer)
 		if err = txcommon.AddTokenTransferInputs(op, &s.transfer.TokenTransfer); err == nil {
 			err = s.mgr.operations.AddOrReuseOperation(ctx, op)
@@ -262,19 +267,21 @@ func (s *transferSender) sendInternal(ctx context.Context, method sendMethod) (e
 
 func (s *transferSender) buildTransferMessage(ctx context.Context, in *core.MessageInOut) (syncasync.Sender, error) {
 	allowedTypes := []fftypes.FFEnum{
-		core.MessageTypeTransferBroadcast,
-		core.MessageTypeTransferPrivate,
+		core.MessageTypeBroadcast,
+		core.MessageTypePrivate,
+		core.MessageTypeDeprecatedTransferBroadcast,
+		core.MessageTypeDeprecatedTransferPrivate,
 	}
 	if in.Header.Type == "" {
-		in.Header.Type = core.MessageTypeTransferBroadcast
+		in.Header.Type = core.MessageTypeBroadcast
 	}
 	switch in.Header.Type {
-	case core.MessageTypeTransferBroadcast:
+	case core.MessageTypeBroadcast, core.MessageTypeDeprecatedTransferBroadcast:
 		if s.mgr.broadcast == nil {
 			return nil, i18n.NewError(ctx, coremsgs.MsgMessagesNotSupported)
 		}
 		return s.mgr.broadcast.NewBroadcast(in), nil
-	case core.MessageTypeTransferPrivate:
+	case core.MessageTypePrivate, core.MessageTypeDeprecatedTransferPrivate:
 		if s.mgr.messaging == nil {
 			return nil, i18n.NewError(ctx, coremsgs.MsgMessagesNotSupported)
 		}
diff --git a/internal/batch/batch_processor.go b/internal/batch/batch_processor.go
index 2887b8745..011a6559d 100644
--- a/internal/batch/batch_processor.go
+++ b/internal/batch/batch_processor.go
@@ -1,4 +1,4 @@
-// Copyright © 2022 Kaleido, Inc.
+// Copyright © 2023 Kaleido, Inc.
 //
 // SPDX-License-Identifier: Apache-2.0
 //
@@ -614,6 +614,7 @@ func (bp *batchProcessor) markPayloadDispatched(state *DispatchState) error {
 			for i, msg := range state.Messages {
 				msgIDs[i] = msg.Header.ID
 				msg.BatchID = state.Persisted.ID
+				msg.TransactionID = state.Persisted.TX.ID
 				if bp.conf.txType == core.TransactionTypeBatchPin {
 					msg.State = core.MessageStateSent
 				} else {
@@ -634,14 +635,16 @@ func (bp *batchProcessor) markPayloadDispatched(state *DispatchState) error {
 			if bp.conf.txType == core.TransactionTypeBatchPin {
 				// Sent state waiting for confirm
 				allMsgsUpdate = database.MessageQueryFactory.NewUpdate(ctx).
-					Set("batch", state.Persisted.ID).   // Mark the batch they are in
-					Set("state", core.MessageStateSent) // Set them sent, so they won't be picked up and re-sent after restart/rewind
+					Set("batch", state.Persisted.ID).    // Mark the batch they are in
+					Set("state", core.MessageStateSent). // Set them sent, so they won't be picked up and re-sent after restart/rewind
+					Set("txid", state.Persisted.TX.ID)
 			} else {
 				// Immediate confirmation if no batch pinning
 				allMsgsUpdate = database.MessageQueryFactory.NewUpdate(ctx).
 					Set("batch", state.Persisted.ID).
 					Set("state", core.MessageStateConfirmed).
-					Set("confirmed", confirmTime)
+					Set("confirmed", confirmTime).
+					Set("txid", state.Persisted.TX.ID)
 			}
 
 			if err = bp.database.UpdateMessages(ctx, bp.bm.namespace, filter, allMsgsUpdate); err != nil {
diff --git a/internal/broadcast/manager.go b/internal/broadcast/manager.go
index f47234a8c..d6db99676 100644
--- a/internal/broadcast/manager.go
+++ b/internal/broadcast/manager.go
@@ -109,8 +109,8 @@ func NewBroadcastManager(ctx context.Context, ns *core.Namespace, di database.Pl
 			[]core.MessageType{
 				core.MessageTypeBroadcast,
 				core.MessageTypeDefinition,
-				core.MessageTypeTransferBroadcast,
-				core.MessageTypeApprovalBroadcast,
+				core.MessageTypeDeprecatedTransferBroadcast,
+				core.MessageTypeDeprecatedApprovalBroadcast,
 			}, bm.dispatchBatch, bo)
 	}
 
diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go
index 78049c8c1..ba1848de2 100644
--- a/internal/coremsgs/en_struct_descriptions.go
+++ b/internal/coremsgs/en_struct_descriptions.go
@@ -58,6 +58,7 @@ var (
 	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")
+	MessageTxParent        = ffm("MessageHeader.txparent", "The parent transaction that originally triggered this message")
 
 	// Message field descriptions
 	MessageHeader         = ffm("Message.header", "The message header contains all fields that are used to build the message hash")
@@ -68,6 +69,7 @@ var (
 	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")
+	MessageTransactionID  = ffm("Message.txid", "The ID of the transaction used to order/deliver this message")
 	MessageIdempotencyKey = ffm("Message.idempotencyKey", "An optional unique identifier for a message. Cannot be duplicated within a namespace, thus allowing idempotent submission of messages to the API. Local only - not transferred when the message is sent to other members of the network")
 
 	// MessageInOut field descriptions
diff --git a/internal/database/sqlcommon/message_sql.go b/internal/database/sqlcommon/message_sql.go
index a3e97659e..e56cb3471 100644
--- a/internal/database/sqlcommon/message_sql.go
+++ b/internal/database/sqlcommon/message_sql.go
@@ -1,4 +1,4 @@
-// Copyright © 2022 Kaleido, Inc.
+// Copyright © 2023 Kaleido, Inc.
 //
 // SPDX-License-Identifier: Apache-2.0
 //
@@ -51,12 +51,18 @@ var (
 		"state",
 		"confirmed",
 		"tx_type",
+		"tx_id",
+		"tx_parent_type",
+		"tx_parent_id",
 		"batch_id",
 		"idempotency_key",
 	}
 	msgFilterFieldMap = map[string]string{
 		"type":           "mtype",
 		"txtype":         "tx_type",
+		"txid":           "tx_id",
+		"txparent.type":  "tx_parent_type",
+		"txparent.id":    "tx_parent_id",
 		"batch":          "batch_id",
 		"group":          "group_hash",
 		"idempotencykey": "idempotency_key",
@@ -67,6 +73,13 @@ const messagesTable = "messages"
 const messagesDataJoinTable = "messages_data"
 
 func (s *SQLCommon) attemptMessageUpdate(ctx context.Context, tx *dbsql.TXWrapper, message *core.Message) (int64, error) {
+	var txParentID *fftypes.UUID
+	var txParentType core.TransactionType
+	if message.Header.TxParent != nil {
+		txParentID = message.Header.TxParent.ID
+		txParentType = message.Header.TxParent.Type
+	}
+
 	return s.UpdateTx(ctx, messagesTable, tx,
 		sq.Update(messagesTable).
 			Set("cid", message.Header.CID).
@@ -83,6 +96,9 @@ func (s *SQLCommon) attemptMessageUpdate(ctx context.Context, tx *dbsql.TXWrappe
 			Set("state", message.State).
 			Set("confirmed", message.Confirmed).
 			Set("tx_type", message.Header.TxType).
+			Set("tx_id", message.TransactionID).
+			Set("tx_parent_type", txParentType).
+			Set("tx_parent_id", txParentID).
 			Set("batch_id", message.BatchID).
 			Set("idempotency_key", message.IdempotencyKey).
 			Where(sq.Eq{
@@ -97,6 +113,13 @@ func (s *SQLCommon) attemptMessageUpdate(ctx context.Context, tx *dbsql.TXWrappe
 }
 
 func (s *SQLCommon) setMessageInsertValues(query sq.InsertBuilder, message *core.Message) sq.InsertBuilder {
+	var txParentID *fftypes.UUID
+	var txParentType core.TransactionType
+	if message.Header.TxParent != nil {
+		txParentID = message.Header.TxParent.ID
+		txParentType = message.Header.TxParent.Type
+	}
+
 	return query.Values(
 		message.Header.ID,
 		message.Header.CID,
@@ -115,6 +138,9 @@ func (s *SQLCommon) setMessageInsertValues(query sq.InsertBuilder, message *core
 		message.State,
 		message.Confirmed,
 		message.Header.TxType,
+		message.TransactionID,
+		txParentType,
+		txParentID,
 		message.BatchID,
 		message.IdempotencyKey,
 	)
@@ -407,6 +433,7 @@ func (s *SQLCommon) loadDataRefs(ctx context.Context, namespace string, msgs []*
 
 func (s *SQLCommon) msgResult(ctx context.Context, row *sql.Rows) (*core.Message, error) {
 	var msg core.Message
+	var txParent core.TransactionRef
 	err := row.Scan(
 		&msg.Header.ID,
 		&msg.Header.CID,
@@ -425,6 +452,9 @@ func (s *SQLCommon) msgResult(ctx context.Context, row *sql.Rows) (*core.Message
 		&msg.State,
 		&msg.Confirmed,
 		&msg.Header.TxType,
+		&msg.TransactionID,
+		&txParent.Type,
+		&txParent.ID,
 		&msg.BatchID,
 		&msg.IdempotencyKey,
 		// Must be added to the list of columns in all selects
@@ -433,6 +463,9 @@ func (s *SQLCommon) msgResult(ctx context.Context, row *sql.Rows) (*core.Message
 	if err != nil {
 		return nil, i18n.WrapError(ctx, err, coremsgs.MsgDBReadErr, messagesTable)
 	}
+	if txParent.ID != nil {
+		msg.Header.TxParent = &txParent
+	}
 	return &msg, nil
 }
 
diff --git a/internal/events/aggregator.go b/internal/events/aggregator.go
index c7b1b4cd2..8ccfb7cb4 100644
--- a/internal/events/aggregator.go
+++ b/internal/events/aggregator.go
@@ -532,6 +532,18 @@ func (ag *aggregator) processMessage(ctx context.Context, manifest *core.BatchMa
 	return nil
 }
 
+func needsTokenTransfer(msg *core.Message) bool {
+	return (msg.Header.TxParent != nil && msg.Header.TxParent.Type == core.TransactionTypeTokenTransfer) ||
+		msg.Header.Type == core.MessageTypeDeprecatedTransferBroadcast ||
+		msg.Header.Type == core.MessageTypeDeprecatedTransferPrivate
+}
+
+func needsTokenApproval(msg *core.Message) bool {
+	return (msg.Header.TxParent != nil && msg.Header.TxParent.Type == core.TransactionTypeTokenApproval) ||
+		msg.Header.Type == core.MessageTypeDeprecatedApprovalBroadcast ||
+		msg.Header.Type == core.MessageTypeDeprecatedApprovalPrivate
+}
+
 func (ag *aggregator) attemptMessageDispatch(ctx context.Context, msg *core.Message, data core.DataArray, tx *fftypes.UUID, state *batchState, pin *core.Pin) (newState core.MessageState, dispatched bool, err error) {
 	var customCorrelator *fftypes.UUID
 
@@ -548,7 +560,7 @@ func (ag *aggregator) attemptMessageDispatch(ctx context.Context, msg *core.Mess
 		}
 
 		// For transfers, verify the transfer has come through
-		if msg.Header.Type == core.MessageTypeTransferBroadcast || msg.Header.Type == core.MessageTypeTransferPrivate {
+		if needsTokenTransfer(msg) {
 			fb := database.TokenTransferQueryFactory.NewFilter(ctx)
 			filter := fb.And(
 				fb.Eq("message", msg.Header.ID),
@@ -563,7 +575,7 @@ func (ag *aggregator) attemptMessageDispatch(ctx context.Context, msg *core.Mess
 		}
 
 		// For approvals, verify the approval has come through
-		if msg.Header.Type == core.MessageTypeApprovalBroadcast || msg.Header.Type == core.MessageTypeApprovalPrivate {
+		if needsTokenApproval(msg) {
 			fb := database.TokenApprovalQueryFactory.NewFilter(ctx)
 			filter := fb.And(
 				fb.Eq("message", msg.Header.ID),
diff --git a/internal/events/persist_batch.go b/internal/events/persist_batch.go
index 4d0cff6fc..7a9b5131f 100644
--- a/internal/events/persist_batch.go
+++ b/internal/events/persist_batch.go
@@ -108,7 +108,6 @@ func (em *eventManager) validateAndPersistBatchContent(ctx context.Context, batc
 		if valid = em.validateBatchMessage(ctx, batch, i, msg); !valid {
 			return false, nil
 		}
-		msg.LocalNamespace = em.namespace.Name
 	}
 
 	// We require that the batch contains exactly the set of data that is in the messages - no more or less.
@@ -198,7 +197,9 @@ func (em *eventManager) validateBatchMessage(ctx context.Context, batch *core.Ba
 		log.L(ctx).Errorf("Mismatched key/author '%s'/'%s' on message entry %d in batch '%s'", msg.Header.Key, msg.Header.Author, i, batch.ID)
 		return false
 	}
+	msg.LocalNamespace = em.namespace.Name
 	msg.BatchID = batch.ID
+	msg.TransactionID = batch.Payload.TX.ID
 
 	l.Tracef("Batch '%s' message %d: %+v", batch.ID, i, msg)
 
diff --git a/internal/privatemessaging/privatemessaging.go b/internal/privatemessaging/privatemessaging.go
index 060d2fb99..f03f25230 100644
--- a/internal/privatemessaging/privatemessaging.go
+++ b/internal/privatemessaging/privatemessaging.go
@@ -141,8 +141,8 @@ func NewPrivateMessaging(ctx context.Context, ns *core.Namespace, di database.Pl
 		[]core.MessageType{
 			core.MessageTypeGroupInit,
 			core.MessageTypePrivate,
-			core.MessageTypeTransferPrivate,
-			core.MessageTypeApprovalPrivate,
+			core.MessageTypeDeprecatedTransferPrivate,
+			core.MessageTypeDeprecatedApprovalPrivate,
 		},
 		pm.dispatchPinnedBatch, bo)
 
diff --git a/pkg/core/message.go b/pkg/core/message.go
index 38f4b6f02..9a023214e 100644
--- a/pkg/core/message.go
+++ b/pkg/core/message.go
@@ -1,4 +1,4 @@
-// Copyright © 2022 Kaleido, Inc.
+// Copyright © 2023 Kaleido, Inc.
 //
 // SPDX-License-Identifier: Apache-2.0
 //
@@ -42,14 +42,14 @@ var (
 	MessageTypePrivate = fftypes.FFEnumValue("messagetype", "private")
 	// MessageTypeGroupInit is a special private message that contains the definition of the group
 	MessageTypeGroupInit = fftypes.FFEnumValue("messagetype", "groupinit")
-	// MessageTypeTransferBroadcast is a broadcast message to accompany/annotate a token transfer
-	MessageTypeTransferBroadcast = fftypes.FFEnumValue("messagetype", "transfer_broadcast")
-	// MessageTypeTransferPrivate is a private message to accompany/annotate a token transfer
-	MessageTypeTransferPrivate = fftypes.FFEnumValue("messagetype", "transfer_private")
-	// MessageTypeApprovalBroadcast is a broadcast message to accompany/annotate a token approval
-	MessageTypeApprovalBroadcast = fftypes.FFEnumValue("messagetype", "approval_broadcast")
-	// MessageTypeApprovalPrivate is a private message to accompany/annotate a token approval
-	MessageTypeApprovalPrivate = fftypes.FFEnumValue("messagetype", "approval_private")
+	// MessageTypeDeprecatedTransferBroadcast is deprecated - use MessageTypeBroadcast (and refer to TxParent.Type)
+	MessageTypeDeprecatedTransferBroadcast = fftypes.FFEnumValue("messagetype", "transfer_broadcast")
+	// MessageTypeDeprecatedTransferPrivate is deprecated - use MessageTypePrivate (and refer to TxParent.Type)
+	MessageTypeDeprecatedTransferPrivate = fftypes.FFEnumValue("messagetype", "transfer_private")
+	// MessageTypeDeprecatedApprovalBroadcast is deprecated - use MessageTypeBroadcast (and refer to TxParent.Type)
+	MessageTypeDeprecatedApprovalBroadcast = fftypes.FFEnumValue("messagetype", "approval_broadcast")
+	// MessageTypeDeprecatedApprovalPrivate is a deprecated - use MessageTypePrivate (and refer to TxParent.Type)
+	MessageTypeDeprecatedApprovalPrivate = fftypes.FFEnumValue("messagetype", "approval_private")
 )
 
 // MessageState is the current transmission/confirmation state of a message
@@ -84,6 +84,7 @@ type MessageHeader struct {
 	Topics    fftypes.FFStringArray `ffstruct:"MessageHeader" json:"topics,omitempty"`
 	Tag       string                `ffstruct:"MessageHeader" json:"tag,omitempty"`
 	DataHash  *fftypes.Bytes32      `ffstruct:"MessageHeader" json:"datahash,omitempty" ffexcludeinput:"true"`
+	TxParent  *TransactionRef       `ffstruct:"MessageHeader" json:"txparent,omitempty" ffexcludeinput:"true"`
 }
 
 // Message is the envelope by which coordinated data exchange can happen between parties in the network
@@ -94,6 +95,7 @@ type Message struct {
 	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"`
+	TransactionID  *fftypes.UUID         `ffstruct:"Message" json:"txid,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"`
diff --git a/pkg/database/plugin.go b/pkg/database/plugin.go
index 856b243e0..1f5bc3456 100644
--- a/pkg/database/plugin.go
+++ b/pkg/database/plugin.go
@@ -698,6 +698,9 @@ var MessageQueryFactory = &ffapi.QueryFields{
 	"sequence":       &ffapi.Int64Field{},
 	"txtype":         &ffapi.StringField{},
 	"batch":          &ffapi.UUIDField{},
+	"txid":           &ffapi.UUIDField{},
+	"txparent.type":  &ffapi.StringField{},
+	"txparent.id":    &ffapi.UUIDField{},
 }
 
 // BatchQueryFactory filter fields for batches


Test patch:
diff --git a/internal/assets/token_approval_test.go b/internal/assets/token_approval_test.go
index cc81d85db..7c93fab48 100644
--- a/internal/assets/token_approval_test.go
+++ b/internal/assets/token_approval_test.go
@@ -139,7 +139,8 @@ func TestApprovalBadConnector(t *testing.T) {
 			Operator: "operator",
 			Key:      "key",
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 	pool := &core.TokenPool{
 		Locator:   "F1",
@@ -149,14 +150,17 @@ func TestApprovalBadConnector(t *testing.T) {
 
 	mdi := am.database.(*databasemocks.Plugin)
 	mim := am.identity.(*identitymanagermocks.Manager)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mim.On("ResolveInputSigningKey", context.Background(), "key", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil)
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenApproval, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.TokenApproval(context.Background(), approval, false)
 	assert.Regexp(t, "FF10272", err)
 
 	mdi.AssertExpectations(t)
 	mim.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestApprovalDefaultPoolSuccess(t *testing.T) {
@@ -222,9 +226,11 @@ func TestApprovalDefaultPoolNoPool(t *testing.T) {
 			Operator: "operator",
 			Key:      "key",
 		},
+		IdempotencyKey: "idem1",
 	}
 
 	mdi := am.database.(*databasemocks.Plugin)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	fb := database.TokenPoolQueryFactory.NewFilter(context.Background())
 	f := fb.And()
 	f.Limit(1).Count(true)
@@ -237,9 +243,13 @@ func TestApprovalDefaultPoolNoPool(t *testing.T) {
 		info, _ := f.Finalize()
 		return info.Count && info.Limit == 1
 	}))).Return(tokenPools, filterResult, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenApproval, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.TokenApproval(context.Background(), approval, false)
 	assert.Regexp(t, "FF10292", err)
+
+	mdi.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestApprovalBadPool(t *testing.T) {
@@ -252,16 +262,20 @@ func TestApprovalBadPool(t *testing.T) {
 			Operator: "operator",
 			Key:      "key",
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 
 	mdi := am.database.(*databasemocks.Plugin)
-	mim := am.identity.(*identitymanagermocks.Manager)
-	mim.On("ResolveInputSigningKey", context.Background(), "key", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(nil, fmt.Errorf("pop"))
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenApproval, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.TokenApproval(context.Background(), approval, false)
 	assert.EqualError(t, err, "pop")
+
+	mdi.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestApprovalUnconfirmedPool(t *testing.T) {
@@ -273,7 +287,8 @@ func TestApprovalUnconfirmedPool(t *testing.T) {
 			Approved: true,
 			Operator: "operator",
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 	pool := &core.TokenPool{
 		Locator:   "F1",
@@ -282,12 +297,15 @@ func TestApprovalUnconfirmedPool(t *testing.T) {
 	}
 
 	mdi := am.database.(*databasemocks.Plugin)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenApproval, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.TokenApproval(context.Background(), approval, false)
 	assert.Regexp(t, "FF10293", err)
 
 	mdi.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestApprovalIdentityFail(t *testing.T) {
@@ -299,7 +317,8 @@ func TestApprovalIdentityFail(t *testing.T) {
 			Approved: true,
 			Operator: "operator",
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 	pool := &core.TokenPool{
 		Locator:   "F1",
@@ -309,14 +328,17 @@ func TestApprovalIdentityFail(t *testing.T) {
 
 	mdi := am.database.(*databasemocks.Plugin)
 	mim := am.identity.(*identitymanagermocks.Manager)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mim.On("ResolveInputSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("", fmt.Errorf("pop"))
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenApproval, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.TokenApproval(context.Background(), approval, false)
 	assert.EqualError(t, err, "pop")
 
 	mdi.AssertExpectations(t)
 	mim.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestApprovalFail(t *testing.T) {
@@ -372,24 +394,14 @@ func TestApprovalTransactionFail(t *testing.T) {
 		Pool:           "pool1",
 		IdempotencyKey: "idem1",
 	}
-	pool := &core.TokenPool{
-		Locator:   "F1",
-		Connector: "magic-tokens",
-		State:     core.TokenPoolStateConfirmed,
-	}
 
-	mdi := am.database.(*databasemocks.Plugin)
-	mim := am.identity.(*identitymanagermocks.Manager)
 	mth := am.txHelper.(*txcommonmocks.Helper)
-	mim.On("ResolveInputSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil)
-	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil)
 	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenApproval, core.IdempotencyKey("idem1")).Return(nil, fmt.Errorf("pop"))
 
 	_, err := am.TokenApproval(context.Background(), approval, false)
 	assert.EqualError(t, err, "pop")
 
-	mim.AssertExpectations(t)
-	mdi.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestApprovalWithBroadcastMessage(t *testing.T) {
@@ -484,8 +496,13 @@ func TestApprovalWithBroadcastMessageDisabled(t *testing.T) {
 		IdempotencyKey: "idem1",
 	}
 
+	mth := am.txHelper.(*txcommonmocks.Helper)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenApproval, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
+
 	_, err := am.TokenApproval(context.Background(), approval, false)
 	assert.Regexp(t, "FF10415", err)
+
+	mth.AssertExpectations(t)
 }
 
 func TestApprovalWithBroadcastMessageSendFail(t *testing.T) {
@@ -556,7 +573,8 @@ func TestApprovalWithBroadcastPrepareFail(t *testing.T) {
 			Operator: "B",
 			Approved: true,
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 		Message: &core.MessageInOut{
 			InlineData: core.InlineData{
 				{
@@ -568,14 +586,17 @@ func TestApprovalWithBroadcastPrepareFail(t *testing.T) {
 
 	mbm := am.broadcast.(*broadcastmocks.Manager)
 	mms := &syncasyncmocks.Sender{}
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mbm.On("NewBroadcast", approval.Message).Return(mms)
 	mms.On("Prepare", context.Background()).Return(fmt.Errorf("pop"))
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenApproval, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.TokenApproval(context.Background(), approval, false)
 	assert.EqualError(t, err, "pop")
 
 	mbm.AssertExpectations(t)
 	mms.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestApprovalWithPrivateMessage(t *testing.T) {
@@ -594,7 +615,7 @@ func TestApprovalWithPrivateMessage(t *testing.T) {
 			Message: core.Message{
 				Header: core.MessageHeader{
 					ID:   msgID,
-					Type: core.MessageTypeApprovalPrivate,
+					Type: core.MessageTypePrivate,
 				},
 				Hash: hash,
 			},
@@ -654,12 +675,13 @@ func TestApprovalWithPrivateMessageDisabled(t *testing.T) {
 			Operator: "B",
 			Approved: true,
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 		Message: &core.MessageInOut{
 			Message: core.Message{
 				Header: core.MessageHeader{
 					ID:   msgID,
-					Type: core.MessageTypeApprovalPrivate,
+					Type: core.MessageTypeDeprecatedApprovalPrivate,
 				},
 				Hash: hash,
 			},
@@ -671,8 +693,13 @@ func TestApprovalWithPrivateMessageDisabled(t *testing.T) {
 		},
 	}
 
+	mth := am.txHelper.(*txcommonmocks.Helper)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenApproval, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
+
 	_, err := am.TokenApproval(context.Background(), approval, false)
 	assert.Regexp(t, "FF10415", err)
+
+	mth.AssertExpectations(t)
 }
 
 func TestApprovalWithInvalidMessage(t *testing.T) {
@@ -684,7 +711,8 @@ func TestApprovalWithInvalidMessage(t *testing.T) {
 			Operator: "B",
 			Approved: true,
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 		Message: &core.MessageInOut{
 			Message: core.Message{
 				Header: core.MessageHeader{
@@ -699,8 +727,13 @@ func TestApprovalWithInvalidMessage(t *testing.T) {
 		},
 	}
 
+	mth := am.txHelper.(*txcommonmocks.Helper)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenApproval, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
+
 	_, err := am.TokenApproval(context.Background(), approval, false)
 	assert.Regexp(t, "FF10287", err)
+
+	mth.AssertExpectations(t)
 }
 
 func TestApprovalOperationsFail(t *testing.T) {
@@ -878,7 +911,8 @@ func TestApprovalPrepare(t *testing.T) {
 			Operator: "operator",
 			Key:      "key",
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 	pool := &core.TokenPool{
 		Locator:   "F1",
@@ -890,12 +924,15 @@ func TestApprovalPrepare(t *testing.T) {
 
 	mdi := am.database.(*databasemocks.Plugin)
 	mim := am.identity.(*identitymanagermocks.Manager)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mim.On("ResolveInputSigningKey", context.Background(), "key", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil)
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenApproval, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	err := sender.Prepare(context.Background())
 	assert.NoError(t, err)
 
 	mdi.AssertExpectations(t)
 	mim.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
diff --git a/internal/assets/token_transfer_test.go b/internal/assets/token_transfer_test.go
index d02078814..ed84218b2 100644
--- a/internal/assets/token_transfer_test.go
+++ b/internal/assets/token_transfer_test.go
@@ -117,7 +117,8 @@ func TestMintTokensBadConnector(t *testing.T) {
 		TokenTransfer: core.TokenTransfer{
 			Amount: *fftypes.NewFFBigInt(5),
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 	pool := &core.TokenPool{
 		Connector: "bad",
@@ -126,14 +127,17 @@ func TestMintTokensBadConnector(t *testing.T) {
 
 	mdi := am.database.(*databasemocks.Plugin)
 	mim := am.identity.(*identitymanagermocks.Manager)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mim.On("ResolveInputSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil)
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.MintTokens(context.Background(), mint, false)
 	assert.Regexp(t, "FF10272.*bad", err)
 
 	mdi.AssertExpectations(t)
 	mim.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestMintTokenDefaultPoolSuccess(t *testing.T) {
@@ -194,9 +198,11 @@ func TestMintTokenDefaultPoolNoPools(t *testing.T) {
 		TokenTransfer: core.TokenTransfer{
 			Amount: *fftypes.NewFFBigInt(5),
 		},
+		IdempotencyKey: "idem1",
 	}
 
 	mdi := am.database.(*databasemocks.Plugin)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	fb := database.TokenPoolQueryFactory.NewFilter(context.Background())
 	f := fb.And()
 	f.Limit(1).Count(true)
@@ -209,11 +215,13 @@ func TestMintTokenDefaultPoolNoPools(t *testing.T) {
 		info, _ := f.Finalize()
 		return info.Count && info.Limit == 1
 	}))).Return(tokenPools, filterResult, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.MintTokens(context.Background(), mint, false)
 	assert.Regexp(t, "FF10292", err)
 
 	mdi.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestMintTokenDefaultPoolMultiplePools(t *testing.T) {
@@ -224,9 +232,11 @@ func TestMintTokenDefaultPoolMultiplePools(t *testing.T) {
 		TokenTransfer: core.TokenTransfer{
 			Amount: *fftypes.NewFFBigInt(5),
 		},
+		IdempotencyKey: "idem1",
 	}
 
 	mdi := am.database.(*databasemocks.Plugin)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	fb := database.TokenPoolQueryFactory.NewFilter(context.Background())
 	f := fb.And()
 	f.Limit(1).Count(true)
@@ -246,11 +256,13 @@ func TestMintTokenDefaultPoolMultiplePools(t *testing.T) {
 		info, _ := f.Finalize()
 		return info.Count && info.Limit == 1
 	}))).Return(tokenPools, filterResult, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.MintTokens(context.Background(), mint, false)
 	assert.Regexp(t, "FF10292", err)
 
 	mdi.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestMintTokensGetPoolsError(t *testing.T) {
@@ -261,15 +273,19 @@ func TestMintTokensGetPoolsError(t *testing.T) {
 		TokenTransfer: core.TokenTransfer{
 			Amount: *fftypes.NewFFBigInt(5),
 		},
+		IdempotencyKey: "idem1",
 	}
 
 	mdi := am.database.(*databasemocks.Plugin)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mdi.On("GetTokenPools", context.Background(), "ns1", mock.Anything).Return(nil, nil, fmt.Errorf("pop"))
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.MintTokens(context.Background(), mint, false)
 	assert.EqualError(t, err, "pop")
 
 	mdi.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestMintTokensBadPool(t *testing.T) {
@@ -280,16 +296,20 @@ func TestMintTokensBadPool(t *testing.T) {
 		TokenTransfer: core.TokenTransfer{
 			Amount: *fftypes.NewFFBigInt(5),
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 
 	mdi := am.database.(*databasemocks.Plugin)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(nil, fmt.Errorf("pop"))
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.MintTokens(context.Background(), mint, false)
 	assert.EqualError(t, err, "pop")
 
 	mdi.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestMintTokensIdentityFail(t *testing.T) {
@@ -300,7 +320,8 @@ func TestMintTokensIdentityFail(t *testing.T) {
 		TokenTransfer: core.TokenTransfer{
 			Amount: *fftypes.NewFFBigInt(5),
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 	pool := &core.TokenPool{
 		Connector: "magic-tokens",
@@ -309,14 +330,17 @@ func TestMintTokensIdentityFail(t *testing.T) {
 
 	mdi := am.database.(*databasemocks.Plugin)
 	mim := am.identity.(*identitymanagermocks.Manager)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mim.On("ResolveInputSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("", fmt.Errorf("pop"))
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.MintTokens(context.Background(), mint, false)
 	assert.EqualError(t, err, "pop")
 
 	mdi.AssertExpectations(t)
 	mim.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestMintTokensFail(t *testing.T) {
@@ -482,7 +506,8 @@ func TestBurnTokensIdentityFail(t *testing.T) {
 		TokenTransfer: core.TokenTransfer{
 			Amount: *fftypes.NewFFBigInt(5),
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 	pool := &core.TokenPool{
 		Connector: "magic-tokens",
@@ -491,14 +516,17 @@ func TestBurnTokensIdentityFail(t *testing.T) {
 
 	mdi := am.database.(*databasemocks.Plugin)
 	mim := am.identity.(*identitymanagermocks.Manager)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mim.On("ResolveInputSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("", fmt.Errorf("pop"))
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.BurnTokens(context.Background(), burn, false)
 	assert.EqualError(t, err, "pop")
 
 	mdi.AssertExpectations(t)
 	mim.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestBurnTokensConfirm(t *testing.T) {
@@ -596,7 +624,8 @@ func TestTransferTokensUnconfirmedPool(t *testing.T) {
 			To:     "B",
 			Amount: *fftypes.NewFFBigInt(5),
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 	pool := &core.TokenPool{
 		Locator:   "F1",
@@ -605,12 +634,15 @@ func TestTransferTokensUnconfirmedPool(t *testing.T) {
 	}
 
 	mdi := am.database.(*databasemocks.Plugin)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.TransferTokens(context.Background(), transfer, false)
 	assert.Regexp(t, "FF10293", err)
 
 	mdi.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestTransferTokensIdentityFail(t *testing.T) {
@@ -623,7 +655,8 @@ func TestTransferTokensIdentityFail(t *testing.T) {
 			To:     "B",
 			Amount: *fftypes.NewFFBigInt(5),
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 	pool := &core.TokenPool{
 		Connector: "magic-tokens",
@@ -632,14 +665,17 @@ func TestTransferTokensIdentityFail(t *testing.T) {
 
 	mdi := am.database.(*databasemocks.Plugin)
 	mim := am.identity.(*identitymanagermocks.Manager)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mim.On("ResolveInputSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("", fmt.Errorf("pop"))
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.TransferTokens(context.Background(), transfer, false)
 	assert.EqualError(t, err, "pop")
 
 	mdi.AssertExpectations(t)
 	mim.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestTransferTokensNoFromOrTo(t *testing.T) {
@@ -647,7 +683,8 @@ func TestTransferTokensNoFromOrTo(t *testing.T) {
 	defer cancel()
 
 	transfer := &core.TokenTransferInput{
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 	pool := &core.TokenPool{
 		Connector: "magic-tokens",
@@ -656,14 +693,17 @@ func TestTransferTokensNoFromOrTo(t *testing.T) {
 
 	mdi := am.database.(*databasemocks.Plugin)
 	mim := am.identity.(*identitymanagermocks.Manager)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mim.On("ResolveInputSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil)
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.TransferTokens(context.Background(), transfer, false)
 	assert.Regexp(t, "FF10280", err)
 
 	mdi.AssertExpectations(t)
 	mim.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestTransferTokensTransactionFail(t *testing.T) {
@@ -679,24 +719,13 @@ func TestTransferTokensTransactionFail(t *testing.T) {
 		Pool:           "pool1",
 		IdempotencyKey: "idem1",
 	}
-	pool := &core.TokenPool{
-		Locator:   "F1",
-		Connector: "magic-tokens",
-		State:     core.TokenPoolStateConfirmed,
-	}
 
-	mdi := am.database.(*databasemocks.Plugin)
-	mim := am.identity.(*identitymanagermocks.Manager)
 	mth := am.txHelper.(*txcommonmocks.Helper)
-	mim.On("ResolveInputSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil)
-	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil)
 	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(nil, fmt.Errorf("pop"))
 
 	_, err := am.TransferTokens(context.Background(), transfer, false)
 	assert.EqualError(t, err, "pop")
 
-	mim.AssertExpectations(t)
-	mdi.AssertExpectations(t)
 	mth.AssertExpectations(t)
 }
 
@@ -777,7 +806,8 @@ func TestTransferTokensWithBroadcastMessageDisabled(t *testing.T) {
 			To:     "B",
 			Amount: *fftypes.NewFFBigInt(5),
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 		Message: &core.MessageInOut{
 			Message: core.Message{
 				Header: core.MessageHeader{
@@ -793,8 +823,13 @@ func TestTransferTokensWithBroadcastMessageDisabled(t *testing.T) {
 		},
 	}
 
+	mth := am.txHelper.(*txcommonmocks.Helper)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
+
 	_, err := am.TransferTokens(context.Background(), transfer, false)
 	assert.Regexp(t, "FF10415", err)
+
+	mth.AssertExpectations(t)
 }
 
 func TestTransferTokensWithBroadcastMessageSendFail(t *testing.T) {
@@ -867,7 +902,8 @@ func TestTransferTokensWithBroadcastPrepareFail(t *testing.T) {
 			To:     "B",
 			Amount: *fftypes.NewFFBigInt(5),
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 		Message: &core.MessageInOut{
 			InlineData: core.InlineData{
 				{
@@ -879,14 +915,17 @@ func TestTransferTokensWithBroadcastPrepareFail(t *testing.T) {
 
 	mbm := am.broadcast.(*broadcastmocks.Manager)
 	mms := &syncasyncmocks.Sender{}
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mbm.On("NewBroadcast", transfer.Message).Return(mms)
 	mms.On("Prepare", context.Background()).Return(fmt.Errorf("pop"))
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.TransferTokens(context.Background(), transfer, false)
 	assert.EqualError(t, err, "pop")
 
 	mbm.AssertExpectations(t)
 	mms.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestTransferTokensWithPrivateMessage(t *testing.T) {
@@ -906,7 +945,7 @@ func TestTransferTokensWithPrivateMessage(t *testing.T) {
 			Message: core.Message{
 				Header: core.MessageHeader{
 					ID:   msgID,
-					Type: core.MessageTypeTransferPrivate,
+					Type: core.MessageTypePrivate,
 				},
 				Hash: hash,
 			},
@@ -967,12 +1006,13 @@ func TestTransferTokensWithPrivateMessageDisabled(t *testing.T) {
 			To:     "B",
 			Amount: *fftypes.NewFFBigInt(5),
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 		Message: &core.MessageInOut{
 			Message: core.Message{
 				Header: core.MessageHeader{
 					ID:   msgID,
-					Type: core.MessageTypeTransferPrivate,
+					Type: core.MessageTypeDeprecatedTransferPrivate,
 				},
 				Hash: hash,
 			},
@@ -984,8 +1024,13 @@ func TestTransferTokensWithPrivateMessageDisabled(t *testing.T) {
 		},
 	}
 
+	mth := am.txHelper.(*txcommonmocks.Helper)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
+
 	_, err := am.TransferTokens(context.Background(), transfer, false)
 	assert.Regexp(t, "FF10415", err)
+
+	mth.AssertExpectations(t)
 }
 
 func TestTransferTokensWithInvalidMessage(t *testing.T) {
@@ -998,7 +1043,8 @@ func TestTransferTokensWithInvalidMessage(t *testing.T) {
 			To:     "B",
 			Amount: *fftypes.NewFFBigInt(5),
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 		Message: &core.MessageInOut{
 			Message: core.Message{
 				Header: core.MessageHeader{
@@ -1013,8 +1059,13 @@ func TestTransferTokensWithInvalidMessage(t *testing.T) {
 		},
 	}
 
+	mth := am.txHelper.(*txcommonmocks.Helper)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
+
 	_, err := am.TransferTokens(context.Background(), transfer, false)
 	assert.Regexp(t, "FF10287", err)
+
+	mth.AssertExpectations(t)
 }
 
 func TestTransferTokensConfirm(t *testing.T) {
@@ -1152,16 +1203,20 @@ func TestTransferTokensPoolNotFound(t *testing.T) {
 			To:     "B",
 			Amount: *fftypes.NewFFBigInt(5),
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 
 	mdi := am.database.(*databasemocks.Plugin)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(nil, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	_, err := am.TransferTokens(context.Background(), transfer, false)
 	assert.Regexp(t, "FF10109", err)
 
 	mdi.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
 
 func TestTransferPrepare(t *testing.T) {
@@ -1175,7 +1230,8 @@ func TestTransferPrepare(t *testing.T) {
 			To:     "B",
 			Amount: *fftypes.NewFFBigInt(5),
 		},
-		Pool: "pool1",
+		Pool:           "pool1",
+		IdempotencyKey: "idem1",
 	}
 	pool := &core.TokenPool{
 		Connector: "magic-tokens",
@@ -1186,12 +1242,15 @@ func TestTransferPrepare(t *testing.T) {
 
 	mdi := am.database.(*databasemocks.Plugin)
 	mim := am.identity.(*identitymanagermocks.Manager)
+	mth := am.txHelper.(*txcommonmocks.Helper)
 	mim.On("ResolveInputSigningKey", context.Background(), "", identity.KeyNormalizationBlockchainPlugin).Return("0x12345", nil)
 	mdi.On("GetTokenPool", context.Background(), "ns1", "pool1").Return(pool, nil)
+	mth.On("SubmitNewTransaction", context.Background(), core.TransactionTypeTokenTransfer, core.IdempotencyKey("idem1")).Return(fftypes.NewUUID(), nil)
 
 	err := sender.Prepare(context.Background())
 	assert.NoError(t, err)
 
 	mdi.AssertExpectations(t)
 	mim.AssertExpectations(t)
+	mth.AssertExpectations(t)
 }
diff --git a/internal/broadcast/manager_test.go b/internal/broadcast/manager_test.go
index 4b4080707..5ed4d95d0 100644
--- a/internal/broadcast/manager_test.go
+++ b/internal/broadcast/manager_test.go
@@ -66,8 +66,8 @@ func newTestBroadcastCommon(t *testing.T, metricsEnabled bool) (*broadcastManage
 		[]core.MessageType{
 			core.MessageTypeBroadcast,
 			core.MessageTypeDefinition,
-			core.MessageTypeTransferBroadcast,
-			core.MessageTypeApprovalBroadcast,
+			core.MessageTypeDeprecatedTransferBroadcast,
+			core.MessageTypeDeprecatedApprovalBroadcast,
 		}, mock.Anything, mock.Anything).Return()
 	mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything)
 
diff --git a/internal/database/sqlcommon/message_sql_test.go b/internal/database/sqlcommon/message_sql_test.go
index 19ffa8e21..d906b620f 100644
--- a/internal/database/sqlcommon/message_sql_test.go
+++ b/internal/database/sqlcommon/message_sql_test.go
@@ -61,6 +61,10 @@ func TestUpsertE2EWithDB(t *testing.T) {
 			Group:     nil,
 			DataHash:  fftypes.NewRandB32(),
 			TxType:    core.TransactionTypeUnpinned,
+			TxParent: &core.TransactionRef{
+				Type: core.TransactionTypeTokenTransfer,
+				ID:   fftypes.NewUUID(),
+			},
 		},
 		Hash:      fftypes.NewRandB32(),
 		State:     core.MessageStateStaged,
@@ -109,6 +113,10 @@ func TestUpsertE2EWithDB(t *testing.T) {
 			Group:     gid,
 			DataHash:  fftypes.NewRandB32(),
 			TxType:    core.TransactionTypeBatchPin,
+			TxParent: &core.TransactionRef{
+				Type: core.TransactionTypeTokenTransfer,
+				ID:   fftypes.NewUUID(),
+			},
 		},
 		Hash:           fftypes.NewRandB32(),
 		Pins:           []string{fftypes.NewRandB32().String(), fftypes.NewRandB32().String()},
@@ -554,7 +562,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", "ns1", "t1", "c1", nil, b32.String(), b32.String(), b32.String(), "confirmed", 0, "pin", nil, "bob", 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, "", nil, nil, "bob", 0))
 	mock.ExpectQuery("SELECT .*").WillReturnError(fmt.Errorf("pop"))
 	_, err := s.GetMessageByID(context.Background(), "ns1", msgID)
 	assert.Regexp(t, "FF00176", err)
@@ -601,7 +609,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", "ns1", "t1", "c1", nil, b32.String(), b32.String(), b32.String(), "confirmed", 0, "pin", nil, "bob", 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, "", nil, nil, "bob", 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/events/aggregator_test.go b/internal/events/aggregator_test.go
index f4da43bf6..8e9fdbfed 100644
--- a/internal/events/aggregator_test.go
+++ b/internal/events/aggregator_test.go
@@ -1515,7 +1515,7 @@ func TestAttemptMessageDispatchMissingTransfers(t *testing.T) {
 	msg := &core.Message{
 		Header: core.MessageHeader{
 			ID:   fftypes.NewUUID(),
-			Type: core.MessageTypeTransferBroadcast,
+			Type: core.MessageTypeDeprecatedTransferBroadcast,
 			SignerRef: core.SignerRef{
 				Author: org1.DID,
 				Key:    "0x12345",
@@ -1541,7 +1541,7 @@ func TestAttemptMessageDispatchGetTransfersFail(t *testing.T) {
 	msg := &core.Message{
 		Header: core.MessageHeader{
 			ID:        fftypes.NewUUID(),
-			Type:      core.MessageTypeTransferBroadcast,
+			Type:      core.MessageTypeDeprecatedTransferBroadcast,
 			SignerRef: core.SignerRef{Key: "0x12345", Author: org1.DID},
 		},
 	}
@@ -1561,7 +1561,7 @@ func TestAttemptMessageDispatchTransferMismatch(t *testing.T) {
 	msg := &core.Message{
 		Header: core.MessageHeader{
 			ID:        fftypes.NewUUID(),
-			Type:      core.MessageTypeTransferBroadcast,
+			Type:      core.MessageTypeDeprecatedTransferBroadcast,
 			SignerRef: core.SignerRef{Key: "0x12345", Author: org1.DID},
 		},
 	}
@@ -1594,7 +1594,7 @@ func TestAttemptMessageDispatchGetApprovalsFail(t *testing.T) {
 	msg := &core.Message{
 		Header: core.MessageHeader{
 			ID:        fftypes.NewUUID(),
-			Type:      core.MessageTypeApprovalBroadcast,
+			Type:      core.MessageTypeDeprecatedApprovalBroadcast,
 			SignerRef: core.SignerRef{Key: "0x12345", Author: org1.DID},
 		},
 	}
@@ -1614,7 +1614,7 @@ func TestAttemptMessageDispatchApprovalMismatch(t *testing.T) {
 	msg := &core.Message{
 		Header: core.MessageHeader{
 			ID:        fftypes.NewUUID(),
-			Type:      core.MessageTypeApprovalBroadcast,
+			Type:      core.MessageTypeDeprecatedApprovalBroadcast,
 			SignerRef: core.SignerRef{Key: "0x12345", Author: org1.DID},
 		},
 	}
diff --git a/internal/privatemessaging/privatemessaging_test.go b/internal/privatemessaging/privatemessaging_test.go
index 6218fb146..2db370592 100644
--- a/internal/privatemessaging/privatemessaging_test.go
+++ b/internal/privatemessaging/privatemessaging_test.go
@@ -70,8 +70,8 @@ func newTestPrivateMessagingCommon(t *testing.T, metricsEnabled bool) (*privateM
 		[]core.MessageType{
 			core.MessageTypeGroupInit,
 			core.MessageTypePrivate,
-			core.MessageTypeTransferPrivate,
-			core.MessageTypeApprovalPrivate,
+			core.MessageTypeDeprecatedTransferPrivate,
+			core.MessageTypeDeprecatedApprovalPrivate,
 		}, mock.Anything, mock.Anything).Return()
 
 	mba.On("RegisterDispatcher",