Actor-based contract model aims at solving some of the outstanding issues in current call-based contract model in backward compatibility, fee payment and other areas.
Actor-based account model is based on the realization that contract invocations are of two completely different types, and they have different safety requirements:
Library calls: This invokes code as internal functionality, which is trusted.
External calls: This invokes external contracts to send funds or other information. It is not trusted.
In actor-based account model, contracts send asynchronous messages to each other for external calls. The caller only pays the fees for sending and storing the message, while the receiver later handles the message execution themselves.
Instead of one-type-fit-all account model like it is in Ethereum, in actor-based account model divides contracts into two different types: actor contracts, and library contracts.
Library contracts are code blocks that are specifically only designed for calling as libraries. Library contracts can only be invoked by other library contracts or actor contracts, and cannot be invoked by externally owned accounts. It has the following information:
Code hash: Code of the account.
Note the explict omission of other information such as account balance, nonce, or storage values. Calling library contracts use specialized functions or opcodes, and any other invocations result in fatal error of the execution.Library contract is optional in actor-based contract model, and it can be omitted.
Actor contracts are those that can be sent messages by externally owned accounts or other actor contracts.Actors are encouraged to be small and cheap to execute, for separation of concerns and decreased likelihood of bugs. It has the following information:
Basic: This includes basic information such as account balance and nonce.
Code hash: Code of the account.
Child storage: Storage values of the account.
Messages: Collection of unhandled messages sent to this contract.
Actors can spawn new actors, which act as the child actor of the spawner. Child actors can be killed by parents, which results all messages and storages being discarded.
Because we will be having a large number of actors, they must be cheap to create and to execute. Actor groups, forming a group of actors, as the basic entity of smart contracts, are to ensure actors are not expensive to create and to execute.
An account with an address, that is, in the traditional sense of a single entity of a smart contract, contains a group of actors. Those actors are with independent storage values, but of the same address and share the same fee deduction routine. That single group is also considered a single process in the scheduler.
Actors in an actor group reference each other by a reference index, rather than addresses. Actors in an actor group can be externalized to form a new group, in order to separate fee deduction routine and addresses.
When an externally-owned account (EOA) sends messages to an address, the "main" actor within the actor group receives the message. EOAs cannot send messages to other actors in the actor group.
Messages are the asynchronous communication method for actor contracts.
Actors can also be passed around to different actor groups. The result actor will immediately become a child of the receiving actor, and the receiving actor can then decide to keep or drop the actors. Allow message passing of actors is critical to enable the fetcher pattern in actor design.
It has the following information:
Sender: The direct sender of the message, either another actor contract or an externally owned account.
Data: Bytearray sent alongside the message, up to receiver’s interpretation.
Balance: Money sent alongside the message.
Actors: Actors to be sent alongside the message.
For a specific actor, it has functionality to modify its own storage, create child actors, and send messages.
Storage-related externalities include:
get_storage(key) → value: Get storage value indexed by
set_storage(key, value): Set storage value indexed by
Actor-related externalities include:
create_actor(code_hash) → index: Create a new actor, marking it as the child of the current actor. Return the index of the actor.
kill_actor(index): Kill a direct child actor immediately, referenced by index, dropping all messages of that particular actor.
store_actor(message_index) → index: Store the actor passed by message (indexed by
message_index) as the child actor of the current actor, referenced by new
Message-related externalities include:
get_message() → message: Get a descriptor of the current handling message.
send_system_message(name, message): Send a message to system actor of
send_address_message(address, message): Send a message to external `address’s main actor.
send_child_message(index, message): Send a message to a child actor with
Below are some descriptions of system actors.
Event system actors are actors that turn messages into system events. Accepts any message, and emit an event wrapped with that message.
Actors can create new actors themselves, but they rely on system actors to externalize those actors to create new contracts. When this system actor receives a message with an encoded actor, it generates a new contract address, and put the encoded actor as the main actor of the new actor group of the contract address.
Message processing loop of actor contracts are handled separately outside of transactions. A scheduling algorithm calls actors that have pending messages to handle at the end of every block, and fill up to the block gas limit. This is argubly fairer, because it avoids the problem that miners can choose which contracts are executed and which are not (however, miners can still choose which messages to put on chain).
The goal of the actor message loop scheduler is to accomplish fair sharing of the network. If a big smart contract is deployed on the network, we want to ensure that a sudden boost of usage of that smart contract does not break the whole network.
Scheduler handles gas metering. There is a gas limit enforced globally on the scheduler. All message loop processing of actors must succeed. An out-of-gas error would revert all states of the loop processing. Due to the side-effect-free nature of actor contracts, the error would place the actor into "dead under current gas limit" category, and the scheduler will not attempt to execute it again unless the gas limit has changed.
Under the block gas limit, the job of the scheduler is to allocate those gases into message processing loops. The scheduler, similar to Linux’s CFS scheduler, aims at modelling an "ideal, precise multi-tasking CPU". Instead of measuring processes by time passed, we measure actors by gas consumed.
One of the limitations for actor-based smart contract model is that it will require more on-chain storage. Information about callbacks, without call staks, must be stored in the state machine. Actors must also be able to handle multiple tasks, when it has to wait for something, because messages may not come in sequence.
The principle for building actors is that each actor should only handle a single thing. Build one actor for each address that needs to have an ERC20 token, rather than only a single actor for the whole ERC20 token. Make one actor represent each kitty of CryptoKitties, rather than only a single actor for the whole CryptoKitties. This design will also ease upgrade of smart contracts, because newer actors and older actors can co-exist.
Actors have to fetch information from other actors in many situations. For this, we have the information fetcher pattern. The information fetcher has two responsibilities. First, it acts as a "authenticated" promise that will eventually be passed back to the parent actor. Second, it fetches information that the parent actor needs.
The information fetcher pattern avoids the need for the parent actor to keep storage values of the fetching. Fetcher will be passed to other actor groups who will handle the fees, until it is passed back.
Information fetcher is actor model’s equivalent of contract call stacks.
With actor-based contract model, immutability guarantee becomes much easier to enforce (and in fact, really hard to break). The only thing we need to ensure is a stable interface of message calls. After that, it is simply about assigning each actor with its own VM execution version.
The actor-based message passing account model allows much better security when doing offchain execution.
In an offchain execution environment, we have validators, who handle the actual execution of the smart contract code. The validators generate signed receipt with changes of storage values and message passings. Normal nodes only need to apply those storage values and message passings. If anyone believes that a validator provided an invalid receipt, they can submit a proof on-chain to slash the validator. After that, the state of that smart contract reverts back to the point before invalidation.
Note that in the case of contracts sending messages to other contracts, all related contract states will have to be reverted all together, and with messages reapplied.
Special thanks to the insightful discussions with Sergei Shulepov, Moonbeam team, and phyro on this topic, and inspiration of prior work Primea.