Understanding TXes - Transfer

January 4, 2024

Symbol transfer transactions are used to send mosaics and/or messages from one account to another. They can be standalone transactions or embedded in aggregate transactions. For a deeper understanding of transactions, it's recommended to read Chapter 6 of the Symbol Technical Reference.

For the purposes of this article, we're going to inspect the parts of a transfer transaction and learn about the decisions that went into designing it. The full schema can be found here. Let's begin!

Static Properties

TRANSACTION_VERSION = make_const(uint8, 1)
TRANSACTION_TYPE = make_const(TransactionType, TRANSFER)

The version of the transfer transaction discussed in this article is 1. The TRANSFER transaction type is 0x4154 - entity type (0x1 << 14), code (0x1 << 8), facility code (0x54).

Layout

# recipient address
recipient_address = UnresolvedAddress

recipient_address is the address of the account that will receive the mosaics and/or message contained in the transaction. Notice that it is of type UnresolvedAddress. This indicates that the address can be an address alias and needs to be resolved by a Symbol client prior to processing. By convention, a field ending in address is an address type.

# size of attached message
message_size = uint16

message_size is the size (in bytes) of the attached message. When this value is zero, there is no message. Notice that it is a 16-bit unsigned integer, so the maximum allowed by the structure is 2^16. Typically, this is limited further by the configuration property maxMessageSize in config-node.properties. For mainnet, this setting has a value of 1024. By convention, a field ending in size is a size in bytes.

# number of attached mosaics
mosaics_count = uint8

mosaics_count is the number of attached mosaics. When this value is zero, there are no mosaics. Notice that it is an 8-bit unsigned integer, so the maximum allowed by the structure is 2^8. By convention, a field ending in count is a number of items.

# reserved padding to align mosaics on 8-byte boundary
transfer_transaction_body_reserved_1 = make_reserved(uint8, 0)

# reserved padding to align mosaics on 8-byte boundary
transfer_transaction_body_reserved_2 = make_reserved(uint32, 0)

Following mosaics_count there are five reserved bytes (one 8-bit and one 32-bit placeholder) that must be zeroed by the sender. These are reserved for future use and may be used for something in the future. You might wonder why these are needed at all. Let's learn!

For best performance at the CPU level, integers must start at a byte boundary that is a multiple of their size. For example, an 8-byte integer must have a start offset that is a multiple of eight, a 4-byte integer must have a start offset that is a multiple of four, etc. Both the standalone transaction header and the embedded transaction header end have a size that is a multiple of eight. Accordingly, the custom transaction payload for each individual transaction starts on an 8-byte boundary.

If we sum up the size of the previous fields - recipient_address (24), message_size (2), mosaics_count (1) - we get twenty-seven. Adding the reserved bytes (5) to that results in thirty-two, which is a multiple of eight. As a result, the next field can be a 8-byte integer (which it is).

# attached mosaics
@sort_key(mosaic_id)
mosaics = array(UnresolvedMosaic, mosaics_count)

mosaics is an array of mosaics that will be transferred from the sender to the recipient. The array can contain zero or more mosaics. It must contain exactly mosaics_count mosaics. The sort_key attribute indicates that mosaics must be sorted by field mosaic_id.

Why are zero mosaics allowed?

This allows a sender to send a message to a recipient without transferring any tokens. This could be used to build a messaging system or message-based NFTs.

Why are multiple mosaics allowed?

This allows a sender to make a single purchase composed of different forms of payment. For example, a sender could transfer YEN tokens and STORE_CREDIT tokens to make a purchase. While this could also be accomplished using multiple transfer transactions in an aggregate transaction, it is always cheaper when using a single transfer transaction (because the total transaction size will be smaller).

An UnresolvedMosaic is a structure composed of an (unresolved) mosaic_id and an amount:

# A quantity of a certain mosaic, specified either through a MosaicId or an alias.
struct UnresolvedMosaic
    # Unresolved mosaic identifier.
    mosaic_id = UnresolvedMosaicId

    # Mosaic amount.
    amount = Amount

Both mosaic_id and amount are 8-byte unsigned values, so the size of UnresolvedMosaic is sixteen bytes. Notice that mosaic_id is of type UnresolvedMosaicId. This indicates that the mosaic id can be a mosaic id alias and needs to be resolved by a Symbol client prior to processing.

# attached message
message = array(uint8, message_size)

message is a byte array composed of zero or more data bytes that is sent from the sender to the recipient. The message data is unstructured and not validated by the Symbol consensus protocol. It must be exactly message_size bytes long. Notice that message is after mosaics. If these fields were reversed, padding would be needed between message and mosaics to ensure that mosaics always starts on an 8-byte boundary. In contrast, the current ordering is more compact because it removes the need for any such padding.

Why is a message part of a transfer transaction?

It is common to send a message along with a payment. For example, the message field could be used to store an invoice. Nonetheless, messages are optional. If there was no message_type, that byte would still be needed (reserved) to allow the proper alignment of mosaics. Since transaction fees are relative to transaction (byte) size, there is no extra cost when a message is not specified.

Why is the message data not validated?

This provides application developers the most flexibility to store anything they want. Unlike NEM, there is no message type (plain or encrypted). Encryption is purely an application level concept. In fact, this flexibility allows different applications to use different types of encryption, if desired.