EOSCommunity.org Forums

Alternatives to deferred transactions

Deferred transactions are a feature of the EOSIO protocol that still exist as of EOSIO 2.1 but were deprecated on October 7, 2019. There are several reasons why deferred transactions were deprecated. Some of those motivations are described in this document. In short, they greatly complicate the protocol and have been a major source of bugs in nodeos, and also there is fundamentally no way to guarantee their successful execution so contract developers are forced to provide a backup mechanism anyway for whatever they were using deferred transactions for.

It is not clear when deferred transactions will be removed from the EOSIO protocol, but eventually they will be removed. So contract developers should take the time to gradually remove their existing dependence on deferred transactions. And at the very least they should not be writing new code that depends on deferred transactions. To do this it is important to discuss how deferred transactions are typically used and what are feasible alternative approaches that do not depend on deferred transactions.

EOSIO reference system contracts use of deferred transactions

The EOSIO reference system contracts have only partly been upgraded to not depend on deferred transactions (as of April 8, 2021). The contracts with the biggest dependency on deferred transactions are eosio.msig and eosio.wrap.

eosio.msig contract

A special release of the contracts has already updated the eosio.msig contract to not use deferred transactions (it uses inline actions instead). That modified eosio.msig still preserves the ability to use time delays as an optional part of the enforcement of the permission authorities; this is a feature of the EOSIO permission system that prior to that special eosio.msig release could only be exercised via deferred transactions. This modified version of the eosio.msig contract is currently deployed on the EOS Public Blockchain.

Note that while the eosio.wrap contract has not yet been modified to avoid deferred transactions, it could follow a very similar pattern as the modified eosio.msig contract (using inline actions rather than a deferred transaction) in order to do so.

Deferred transaction use case 1

The prior discussion on the eosio.msig contract actually demonstrates one of the many possible uses of deferred transactions. Deferred transactions allow scheduling a transaction with some prescribed delay. One of the reasons why someone may want to intentionally delay their transaction from executing is for security reasons. Time is a very important tool in security. For example, though it was not really intended for this purpose, many users have found the 3 day unstaking period useful in protecting the majority of their funds (assuming they kept their owner key separate from their active key).

Deferred transactions are one way of exercising the general time delay feature built into the EOSIO permission system. As mentioned above, the new eosio.msig contract that does not use deferred transactions is able to use that feature as well. The main idea to preserve regardless of how it is implemented is that the proof of someone’s keys being utilized to authorize a specific operation are published to a public place (so that the legitimate owner has a chance to get notified of that authorization event) sufficiently earlier than when that authorization is actually utilized to permit irreversibly executing the operation. Both the old deferred transaction implementation of eosio.msig and the new inline action implementation of eosio.msig follow that principle for general operations (a sequence of actions to execute).

One can also follow that principle in the case of a more specific operation. For example, the 3 day unstaking logic within the main system contract is an example of following that principle. And while the unstaking logic does use deferred transaction, as will be discussed shortly, its use of deferred transactions is merely a convenience (which is a different use of deferred transactions than the one being discussed in this section) and isn’t essential to implementing the security of the time delay.

Main system contract

The main system contract has two places where it still uses deferred transactions: one is in automatically scheduling a deferred transaction to call the eosio::refund action 3 days after unstaking is initiated with the eosio::undelegatebw action; the other is in automatically scheduling a deferred transaction to call the eosio::bidrefund action for the user that was outbid on a premium name auction. Both of these are just conveniences for the users. If those deferred transactions never were scheduled or if they were scheduled but ultimately failed to execute (which does happen in practice often, most likely because the user has CPU resource issues), the user still has a way to achieve the desired side-effects by calling the appropriate action (in this case either eosio::refund or eosio::bidrefund) explicitly at the appropriate time.

The prior paragraph actually demonstrates two additional uses of deferred transactions. I will go into those two use cases in some detail because it is likely that an application you are developing may involve a similar use case, and discussing the use case also enables discussing alternatives to achieving the same end result without the use of deferred transactions.

Deferred transaction use case 2

In the first of the two additional use cases (the one automating the eosio::refund action), it involves a process (unstaking) which has two stages that must be separated by time (3 days). The action that initiates this process can carry out the first stage. But the second stage (or even additional stages if we generalize to a multi-stage process) must occur in a separate transaction. The system contract uses a deferred transaction to schedule that transaction for the appropriate time with the hope that it successfully executes. But since there is no guarantee that it will, it must provide an alternative means of triggering the second stage (that is calling the eosio::refund action explicitly).

Note that nothing prevents some off-chain service from automating the process of calling that action explicitly (some sort of cron job). Of course that service would need to be able to sign for the transaction that authorizes the explicit action that triggers the second stage of the process. If the contract sets up the second stage such that all of the inputs it needs to function were already set up by the first stage which was authorized by the appropriate user, then it may be safe to allow anyone to trigger the second stage (and thus also be the person who pays for the CPU/NET costs for the second stage computation).

There is a potential complication with the RAM resource. If the second stage involves adding more state on behalf of the user who initiated the first stage, then the contract may not be allowed to bill that user for the RAM usage in the second stage if it is handled by an action that is not authorized by that user (e.g. because it was triggered by some other unrelated user). However, if the RAM_RESTRICTIONS protocol feature is activated on the blockchain, then a smart contract is allows to add table rows that are billed to an arbitrary user without having the authorization of that user for the action as long as the net delta in RAM for that user is not positive. So this opens up a trick for the smart contract to employ which reserve the expected RAM that will be needed ahead of time for the user who initiates the first stage during the first stage, even if it won’t actually be utilized until the second or later stages. This allows those later stages to be carried out without the authorization of the original initiating user.

Note that it isn’t necessary to rely on a centralized service to trigger the later stages. In fact, it may even be appropriate for some actions within the contract to potentially trigger the later stages (assuming the timing is appropriate). Both REX and PowerUp use this model to trigger the clean up stages for existing resource rental orders.

For example, when some user executes eosio::powerup, the action will initially spend some time cleaning up to two expired PowerUp orders (which in general would be orders of completely unrelated users). In this case, it is critical to ensure that this second stage cannot fail otherwise that could deny service to the user initiating the original action. A weaker form of that requirement is also important to consider: the computation that is triggered should be (within reason) bounded and somewhat predicable to avoid being unfair to the user initiating the original action.

Of course, it may not be sufficient to just rely on cleanup automatically triggered from another action like eosio::powerup. So the main system contract also adds a eosio::powerupexec which has the sole purpose of cleaning up expired PowerUp orders. Again, one could have an off-chain service whose job is to periodically call this action to ensure all the second stages (cleanup stage) for the orders introduced by each eosio::powerup action have executed. And since anyone can call this action, it does not need to be centralized. It is decentralized since anyone with an incentive (e.g. to remove expired PowerUp orders and make the cost of new PowerUps cheaper) can call the eosio::powerupexec action. One can even imagine creating financial incentives for people to call that action (since it does cost the caller some CPU/NET to call it).

Deferred transaction use case 3

In the second of the two additional use cases mentioned earlier (the one automating the eosio::bidrefund action), it involves a process (outbidding the highest bid on a premium name auction) which has two stages where if the first stage (placing the higher bid using the eosio::bidname action) succeeds, then the second stage (returning the funds from the now outbid offer back to the bidder) is allowed to complete (via the eosio::bidrefund action) on its own time without blocking the first stage especially given that the second stage could fail for reasons that are outside the control of the user initiating the first stage. Notice that it is not appropriate to carry out the second stage that may fail in the same atomic transaction as the first stage (e.g. by doing an inline eosio::bidrefund action from the eosio::bidname action) because this can create a selective denial of service which can advantage an attacker.

In fact this is exactly what happened in an earlier version of the premium name auction code. Prior to version 1.2.1 of the system contracts, the eosio::bidname action would do an inline eosio.token::transfer action to refund the user who was outbid. But because the eosio.token::transfer notifies the recipient of the funds, a malicious user could set a contract that would abort the transaction upon receiving any funds. Then this malicious user could bid for a premium account name with a low amount and ensure that no one could outbid them because those transaction would get aborted by their custom contract. The v1.2.1 release of the system contract included a fix which decoupled the two stages (successfully outbidding and refunding the token of the user that was outbid) by not returning the funds during the eosio::bidname action but merely setting up the table data that can then be used to actually get the funds back in a separate transaction with the eosio::bidrefund action. The change also scheduled a deferred transaction to execute eosio::bidrefund action as soon as possible (the scheduling is done in the eosio::bidname action) as a convenience to the outbid user. Though once again I will stress that this is only a convenience and is not necessary for the pattern to work correctly.

This use case seems quite similar to the prior one in that there are two stages that must be separated into different transactions.

In the prior use case (use case 2), the separation was necessary because the two stages needed to be separated by time. But the second stage was expected to execute successfully (after the appropriate time passed) after the first stage succeeded. And, in fact, for some implementations for carrying out the second stage, it is important to ensure the second stage does not fail.

In this use case (use case 3), the separation is not necessary due to time but because the second stage could potentially fail and yet it is not okay for that failure to prevent the successful execution of the first stage.

Also, I hope it is clear that in both of these two prior use cases, the deferred transactions are not essential to implementing the pattern. Deferred transactions only act as a convenience to prevent an explicit trigger for the second stage. The smart contract should still be designed to function correctly if the deferred transaction fails (which does often happen). And as mentioned before, the automatic triggering of the second stage can still occur even without deferred transactions, e.g. through an off-chain service (or even a decentralized market of such services) meant for that purpose. However, it is worth pointing out that in this use case, unlike in the prior one, the approach taken by REX and PowerUp are not acceptable because there is no guarantee that the second stage will not fail. So the contract should not tie the triggering of the second stage of some prior initiated process, which can fail due to reasons controlled by party A, to an unrelated action that party B wishes to execute.

Other potential uses of deferred transactions and possible alternatives

I am sure there are other use cases for deferred transactions that are not similar in nature to the ones I discuss above. Whatever they are, I am also fairly confident there is a reasonable alternative solution to the problem that does not rely on the deferred transaction feature.

I want this thread to be a place where we collect such use cases and discuss possible alternative implementations and patterns that can satisfy that use case without the use of deferred transactions.

2 Likes

Thank you for this comprehensive post. I learned about the old namebid vulnerability which I wasn’t familiar with.
The issue of replacing delayed transactions is important and has some nuances, smart contracts need more attention and focus from developers than old school database and server side code.
Perhaps this will evolve to a recipe collection or a FAQ of sorts for developers to use as good advice and guidance when they want to achieve certain behaviors.