Q&A - Toshi Mosaic Levy

January 5, 2024

Toshi recently wrote an article about extending the mosaic plugin to support mosaic levies. He did a very good job, but had a few unresolved questions. Let's look at what he did and try to answer them!

Q & A

Which plugin should contain the mosaic levy functionality - the mosaic plugin or the transfer plugin?

As a design principle, plugins should be as self-contained as possible. There are some exceptions, but these are typically for the purpose of extending functionality. For example, the metadata plugin depends on the namespace plugin because it extends a namespace by allowing it to have associated metadata. Likewise, the multisig plugin depends on the aggregate plugin because it uses aggregate transactions to store multiple signatures.

Currently, the transfer and mosaic plugins are completely independent. It is possible to have a network that supports the transfer plugin but not the mosaic plugin (such a network would only support a single, currency mosaic). If the mosaic levy logic was moved into the transfer plugin, this independence would break. The transfer plugin would need to access the MosaicCache that is defined in the mosaic plugin. In order to keep things separate, it is better for mosaic levy functionality to be isolated to the mosaic plugin where it has access to the mosaic cache.

Toshi observed errors when he defined the mosaic levy functionality within the transfer plugin. This is most likely due to the new observer in the transfer plugin attempting to access the MosaicCache. This access fails because the cache is in a different module. In order to enable this, the transfer plugin would need to link against the mosaic plugin. But, for reasons described above, this would be undesirable.

Where to specify the mosaic levy?

In Toshi's implementation, he is using hardcoded values for levy mosaic id and amount. It would be better to allow the user to define these as part of a transaction.

One option would be to introduce a revised MosaicDefinitionTransaction version 2. The levy could be optionally specified as part of the definition. However, this approach is a little clumsy, especially if we want to support changes to the levy.

Let's look at MosaicSupplyChangeTransaction for inspiration. This transaction is used to both set the initial supply of a mosaic as well as make additional supply changes. We can do something similar for associating mosaic levies to mosaics.

Consider a new transaction MosaicLevyChangeTransaction:

# mosaic id
mosaic_id = MosaicId

# levy mosaic
levy_mosaic = UnresolvedMosaic

mosaic_id is the mosaic to which the levy is being added or modified. levy_mosaic is the levy mosaic, composed of the mosaic id and amount.

Transaction Padding

Toshi defines a MosaicLevyTransferTransaction, which initiates a mosaic transfer and payment of a levy. The transaction layout is specified as:

# recipient address
recipient_address = UnresolvedAddress

# mosaic levy
mosaic = UnresolvedMosaic

# reserved padding
mosaic_levy_transfer_transaction_body_reserved = make_reserved(uint8, 0)

Let's examine the layouts of each field:

  • 00..23: recipient_address
  • 24..39: mosaic
  • 40..41: mosaic_levy_transfer_transaction_body_reserved

All fields begin on an 8-byte boundary, and there is no trailing data. As a result, there is no need for the reserved field and it can safely be dropped.

ℹ️ Blocks and Aggregate Transactions automatically pad their component transactions to 8-byte boundaries.

Notifications

In Toshi's article, he defines the following validators and observers:

  • LeviedMosaicValidator: Validator that rejects balance transfers of levied mosaics via transfer transactions. This is actually slightly wrong because BalanceTransferNotification can be raised for multiple reasons, not just from transfer transactions. For example, it is used for payment of rental fees.
  • BalanceLevyTransferValidator: Checks that the sender has the sufficient mosaics to make the transfer and pay the levy. This is effectively a specialization of BalanceValidator.
  • MosaicLevyTransferObserver: Performs the transfer of mosaics from the sender to the recipient and pays the levy fee. This is quite similar to BalanceTransferObserver.

While this all works, we can refactor a little and use less code. Let's look at the notifications being published:

template<typename TTransaction>
void Publish(const TTransaction& transaction, const PublishContext& context, NotificationSubscriber& sub) {
    auto padding = transaction.MosaicLevyTransferTransactionBody_Reserved;
    sub.notify(InternalPaddingNotification(padding));
    sub.notify(MosaicLevyTransferNotification(context.SignerAddress, transaction.RecipientAddress, transaction.Mosaic));
}

We already have a BalanceTransferNotification with appropriate validators and observers to make a balance transfer. Let's use it for transferring the mosaic:

sub.notify(BalanceTransferNotification(
        context.SignerAddress,
        transaction.RecipientAddress,
        transaction.Mosaic.MosaicId,
        transaction.Mosaic.Amount);

⚠️ The LeviedMosaicValidator should be removed for this to work.

Now we just need to make the levy payment. We can't raise a BalanceTransferNotification for it because we don't know the recipient. We'll still need a custom validator (BalanceLevyTransferValidator) and observer (MosaicLevyTransferObserver), but these could be changed to operate on BalanceTransferNotification.

The validator could look up the mosaic being transferred from the mosaic cache. If the mosaic has the Leviable flag set, the validator would check that the sender has sufficient balance to pay the levy. The observer would simply make the levy transfer.

Notice that the custom validators are now much simpler because we reused an existing notification! They have a single responsibility and are much easier to understand and test. This is a good example of the S in SOLID!

Another benefit of reusing BalanceTransferNotification is that levies are always paid; even when the mosaic is sent as part of a transfer transaction. In fact, we don't really need a custom transaction type at all!