This post discusses a potential standard that could be adopted by the EOSIO community to provide a mechanism for smart contracts to dynamically indicate that their action is relevant to one or more EOSIO accounts so that the action can possibly be presented in the history of those accounts. This mechanism is one that smart contracts could use instead of
require_recipient forces dynamics that the smart contract developers may find undesirable; specifically, it allows untrusted third-party code to run which may make the CPU costs of the transaction arbitrarily higher or may even abort the transaction thus preventing the original action from completing in the first place. This post also discusses how the new proposed mechanism, which can be implemented without any EOSIO protocol changes, enables the notified accounts to have some control over what is displayed on their account history feed as a way to protect against spam.
History indexers, which drive the account history feeds of wallets and block explorers, tend to decide whether an action is relevant to a particular account (and therefore is worth showing on the account history feed) based on one of the following limited conditions:
- the account considered is the
receiveraccount of the action;
- or, the account considered is in the
authorizationvector of the action.
The latter condition covers the actions that the user of that account authorized using their credentials. Note that if the permission used in the
authorization vector was satisfied via a delegated permission of some other account, that latter delegated account would likely not be considered a relevant account to the action by the history indexers and therefore the action would not show up in that account’s history despite the fact that keys on that account were used to satisfy the authorization of that action.
The former condition based on the
receiver makes sense in that it will allow users of block explorers to see all the actions executed on a particular contract account. It also handles the case of smart contract code executing due to handling the notification of an action of another smart contract. For example, if
bob had a custom contract to handle receipt of funds via
eosio.token::transfer, then if
alice transferred tokens to
bob, that action would not only show up when exploring the account history of the
eosio.token account but also when exploring that of the
bob account. This would be true even if the contract on
bob did not have any explicit logic to handle the
eosio.token::transfer action since the history indexers aren’t trying to be smart enough to determine what side effects the contract handling the notification may have done. In fact, the history indexers take the simpler approach of considering any notified accounts to be relevant whether or not they even have smart contract code on them. This means the transfer would also show up on the account history feed of the
alice account even if
alice was somehow not an authorizer of the transfer action (this is because the
eosio.token::transfer action calls
require_recipient on both the
from and the
Given how history indexers treat the notifications triggered by
require_recipient, many smart contract developers have ended up using
require_recipient as a mechanism to signal to history indexers that the listed accounts are relevant to this action and therefore this action should show up on their account history pages. The smart contract developers may do this even if they do not intend for code on any smart contracts deployed on the notified accounts to be run. Unfortunately, this approach causes any smart contract code deployed on the notified accounts to run and that means (due to the need to maintain the atomic property of the transaction within which these actions and notifications are executing) that this third-party code could disrupt the intended operations of the action. I discussed in a prior post how this behavior actually caused an attack vector in the original implementation of the
It would be helpful to smart contract developers if they had a mechanism to signal to history indexers the list of EOSIO accounts that are relevant to a particular action so that history solutions could then potentially display the action in the account history of the relevant EOSIO accounts. And, in particular, it is important for this mechanism to notify the listed accounts in a manner that does not have the same behavior as
require_recipient which as discussed earlier could be problematic for certain applications.
(Keep in mind that the desired mechanism is intended to not trigger the execution of third-party code that may be deployed on the notified account. If you want to still have that behavior, you will either need to use
require_recipient or do an inline action to the account directly. In either case, that comes with the consequences that the triggered code can abort the transaction or cause it to consume way more CPU than desired. I have some ideas for how a contract could send an event from an executing action to eventually trigger arbitrary code on the recipient contract without allowing that arbitrary code to block the execution of the current action, but that is not in scope for this post and I will likely write something up for that sometime later.)
This desired mechanism is only about signaling information to off-chain systems and not having other side-effects on-chain. Therefore, there is no need for some new host function in the EOSIO protocol to achieve the stated objective. There are already plenty of mechanisms available to signal information to off-chain processes (with an inline action being the most obvious mechanism). The issue is that for this signaling to be useful, there would need to be some standard that smart contracts, history indexers/solutions, wallets, and block explorers would all adopt. So this post is about proposing an initial iteration of such a standard and getting feedback from the community about it.
The standard requires a new action named
notify to be available to call on the account
eosio.notify. The account
eosio.notify must have an appropriate ABI deployed on it but it may or may not have a contract deployed on it.
If a contract is not deployed on the account, the signature of the action must be the following:
void notify(eosio::name sender_account, eosio::name action_name, const std::vector<eosio::name>& accounts_to_notify);
If a contract is deployed on the account, the blockchain it is deployed on must have activated the
ACTION_RETURN_VALUE protocol feature introduced in EOSIO 2.1 and the signature of the action must be the following:
std::vector<eosio::name> notify(eosio::name sender_account, eosio::name action_name, const std::vector<eosio::name>& accounts_to_notify);
History indexers can check the return value within the action trace of
eosio.notify::notify action. If there is no return value (not to be confused with a return value consisting of an empty vector), then they should behave as if there is no contract deployed on the
eosio.notify account (even if there technically is one).
History indexers should ignore the
eosio.notify::notify action if any of the following conditions are true:
eosio.notify::notifyaction is not an inline action;
eosio.notify::notifyaction is a context-free action;
eosio.notify::notifyaction trace is for a
eosio.notify::notifyaction arguments do not successfully unpack according to the signatures above;
eosio.notify::notifyaction return value (assuming it has one) does not successfully unpack according to the return type of the second signature above;
- the action that sent the
eosio.notify::notifyaction has a
receiverthat does not match the value of the
- the action that sent the
eosio.notify::notifyaction has an action name that does not match the value of the
action_nameargument (however ignore this condition if the action trace that is the creator of the
eosio.notify::notifyaction trace was running in a notify context, i.e.
receiver != act.account).
In addition, the history indexer is allowed to ignore a
eosio.notify::notify action even if none of the above conditions are true if the account that sent the
eosio.notify::notify action is on the history indexer’s subjective blacklist.
If the history indexer is behaving as if there is no contract deployed on the
eosio.notify account, then it should construct the set of accounts pulled from the
accounts_to_notify argument of the
eosio.notify::notify action, add to the set the account included in
sender_account, remove any accounts from the set that are included in its subjective blacklist or that are not valid EOSIO accounts, and finally consider the accounts in the remaining set as relevant to the action that called that
eosio.notify::notify action. If an account is considered relevant to an action, it means that a reference to that action should exist in the history feed of the account.
If the history indexer is behaving as if there is a contract deployed on the
eosio.notify account, then its behavior should be very similar to the above except that it constructs the initial set of accounts from the return value of the
eosio.notify::notify action rather than from the
Smart contracts are expected to use the
eosio.notify::notify action to signal to history indexers the set of accounts they consider relevant to the calling action. They do this by sending an inline
eosio.notify::notify action with the following arguments set according to the following rules:
sender_accountmust be set to the account name of the calling contract (this is typically determined via the
action_namecannot be the empty name;
action_namemust be set to the name of the action currently being processed by the calling contract assuming this is not in a notification context (if this is in a notification context, then the calling contract is free to choose any non-empty name for the
accounts_to_notifymust be in strictly increasing order.
A couple quick clarifications about rule 4: the names do not necessarily need to refer to valid EOSIO accounts; strictly increasing order also means that names cannot be repeated in the vector.
If the calling contract follows rules 1, 2, and 4, they have a guarantee that the
eosio.notify::notify action will never explicitly abort (not counting deadline timeout or CPU/NET exhaustion) assuming that any contract deployed on the
eosio.notify account is compliant with the standard. In addition, while not a guarantee as part of the standard, the calling contract can expect that the computational cost of the
eosio.notify::notify action will scale linearly with the size of
accounts_to_notify and will take a reasonable amount of time. This gives confidence to smart contract developers that it is safe to call the
eosio.notify::notify action regardless of the current state as long as they correctly follow rules 1, 2, and 4.
Smart contract developers are also highly encouraged to follow rule 3. If they do not, then the history indexers are likely to not actually respect their notification request. However, rule 3 is considered separately from rules 1, 2, and 4 because the
eosio.notify contract (assuming it exists) has no way in general to validate the name of the action from which the
eosio.notify::notify inline action was sent. In addition, the standard can make no guarantees about whether a particular history indexer will respect the request to notify the accounts listed in
accounts_to_notify (or for that matter the ones in the return value). This is because each history indexer is allowed to enforce its own local subjective blacklist of accounts.
A smart contract can be deployed to the
eosio.notify account as long as it respects the
eosio.notify action according to the signature defined earlier that has the non-
void return value and it does not fail (as discussed above) if rules 1, 2, and 4 are satisfied.
If the operators of a particular EOSIO blockchain decided to actually deploy a smart contract to the
eosio.notify account, then they would need to ensure it complied with the standard (see last sentence of the previous section). But there are other recommendations I would like to make (that are not actually required by the
eosio.notify standard proposed above) for how this
eosio.notify contract should behave.
First, I would recommend that the
eosio.notify contract enforce rules 1, 2, and 4. It can enforce rule 1 by using the
get_sender() host function, ensuring it is not the empty value, and then ensuring it is identical to the value of
In addition, the smart contract is expected to return a reasonable return value so that intended off-chain behavior of the
notify action can function properly. If the contract always returns the empty vector, then the
notify action will have no use. One option is to just return the same vector provided via
accounts_to_notify. This would result in the same behavior for history indexers as they would have if the smart contract was not deployed at all; however, now if the calling contract fails to satisfy rules 1, 2, and 4, it could be detected at transaction execution time (via aborting the transaction) rather than later when it was noticed that the history solutions were not displaying the actions in the account history feed of the notified accounts.
A more interesting thing that the
notify action could do, however, is to make the return value a subset of the set defined by
accounts_to_notify. It could remove from the specified set any account that was configured (according to configuration state tracked by the
eosio.notify contract) to not receive notifications from the contract on the
sender_account account (possibly modulated by the further context of
action_name). This could allow accounts to opt-in or opt-out of receiving notifications from specific contracts which can help address the spam problem that already exists due to
My recommendation would be to allow both system-level configuration and user-level configuration within the
eosio.notify account. These configurations would allow specifying the pair of the
action_name as a filter (with the
action_name optionally set to empty name to act as a wildcard) which would map to behavior to take: permit notification, deny notification, or inherit decision from ancestor. The user-level configuration would be lower in the inheritance hierarchy than the system-level configuration which means it would have precedence over the system-level decision. At the top of the inheritance hierarchy (and therefore the one with the lowest precedence) would be the default decision to deny notification.
Then the blockchain operators could add system-level filters to permit notification for the few system contracts. That way users would not have to take any action (and use up any RAM) to opt-in to being notified of these important system functions; though they could still opt-out if they desired. However, for other smart contracts, the user would need to use an action on the
eosio.notify contract to opt-in to being notified by that contract. Otherwise, by default, those actions would not show up on their account history feed which helps address the spam issue that would otherwise exist if the default was to permit notification by all contracts.
However, it is important to note that this particular policy decision in the
eosio.notify contract is just one choice and is not required to be compliant with the proposed standard above. Again, as a reminder, it is possible to be compliant with the standard without deploying any contract to the
eosio.notify account (which would be choosing the most permissive policy). There are also CPU benefits in not deploying a contract to the
eosio.notify account because then the inline
eosio.notify::notify action would not require running any WebAssembly code and would therefore take very little time to execute.