The actor model was first theorized in a paper published in 1973 by Carl Hewitt, Peter Bishop, and Richard Steiger:
“[The Actor Model is] motivated by the prospect of highly parallel computing
machines consisting of dozens, hundreds, or even thousands of independent
microprocessors, each with its own local memory and communications processor,
communicating via a high-performance communications network.”
Actors are quite similar to objects. As such, they may or may not hold internal state (which may be either mutable or immutable) and expose methods to interact with and modify it. Differently from objects, though, such methods may not be invoked directly. As a matter of fact, the only way to interact with an actor is by sending it a message.
Messages in the actor model have four main characteristics:
- they are asynchronous (i.e. the sender does not block waiting for a response);
- they are immutable (i.e. they may not be modified after they’re sent);
- they are placed into the actor’s mailbox, which is just a queue of messages to be processed;
- the actor goes through its mailbox one message at a time, processing it in its thread.
These four characteristics ensure that no race condition will happen. As a matter of fact, messages are the only way to interact and modify the internal state of an actor. However, being such messages processed one at a time by the actor itself (in a single thread), there is no possibility of race conditions (unless a reference to the state is passed as an argument of a message, in which case the actor loses the control over its state).
Lastly, there’s no one-to-one correspondence between an actor and a thread. In fact, actors just “borrow” threads from a pool.
A bit of terminology
Smart contracts are applications that use blockchain platforms to perform certain actions, such as exchanging information or money without relying on trusted third-party entities.
A smart contract is very much like a class in the object-oriented programming paradigm and, as such, it has a state and exposes a set of functions. Invoking functions is the only way to “activate” a smart contract and normally happens in the context of a transaction.
A concurrent perspective
At first glance, smart contracts and actors have many similarities:
- state mutations happen as a result of a message/transaction;
- messages are serialized and processed one at a time;
- messages are immutable;
- an address is needed to interact with an actor or a contract, and addresses are unique;
- actors and contracts can decide what to do with a message, possibly rejecting it;
- they can both generate new actors/contracts;
- they both have to be killed and regenerated to fix a bug in their behavior. For contracts, this is more delicate, though, as they also hold a balance that has to be taken care of.
However, there are some notable differences as well:
- a smart contract might very well expose its state, which will be easily modifiable by any other contract holding a reference to it. This is done, as usual, by making state variables
- contracts hold a balance, which has to be carefully handled in order not to incur in unpleasant situations (read The DAO);
- actors are concurrent and asynchronous by default, whereas smart contracts are not (within a transaction). This means that only the first function invocation (the one that originates the transaction) is run asynchronously, but from that moment on any function calls happening within that transaction will be synchronous;
- linked to the previous bullet point, the transaction boundary in the actor system is the single actor. This is not true for smart contracts, as a transaction can span multiple invocations of different contracts;
- transactions and state changes are guaranteed to be persisted in the blockchain (for smart contracts), whereas this might not be true for actors;
- the execution of smart contracts, in Ethereum, is bounded by the so-called gas.
As we just saw, actors and smart contracts are quite similar in the way they handle incoming messages, but deeply differ in their concurrent behavior and in its impact in keeping the internal state consistent. As a matter of fact, in his paper, Ilya Sergey describes the similarities between concurrent objects and smart contracts, highlighting how bugs due to common clumsy implementation of the former also might affect the latter.
How “oracles” make things worse
Smart contracts’ “dual” concurrent behavior also poses some challenges when it comes to deal with programs outside the blockchain. In Ethereum, the common solution is the so-called “oracle pattern”, with oracles being entities that are authorized to send data to a contract (by invoking one of its functions). The blockchain doesn’t verify the authenticity of that data, but all nodes are able to agree on it.
This sounds good on paper, but there’s trouble in Paradise. Any amount of time might pass between the call to the oracle and the callback to the contract. During this time, the contract might have changed its state and that might affect the way oracle’s output is processed.
This problem is not only related to oracles though. As you might recall, smart contracts have been suffering from reentrancy issues, for example, those involved in The DAO.
In both cases, the issue originates from the fact that Solidity does not give any help in enforcing protocols.
The original formulation of the actor model does not mention protocols or how to enforce them. The same applies to Ethereum and Solidity.
A synonym of “protocol” I particularly like is “agreement”: a protocol is just an agreement, between two or more parties, on how to carry something out. In the case of oracles, it might involve the expected state when the oracle calls back the contract. This is usually done by modeling the involved parties as state machines, each of which exposes specific functionality (i.e. accepts specific messages) in each state.
The usual example is a File:
- the initial state is
- it can be
opened only when it is in
- once it is
Open, it can be used for
writeoperations and it can also be
The so-called “typestate-oriented” programming allows for the definition of protocols in object-oriented programming languages without requiring too much boilerplate code. When it comes to type system, they should keep track of the state the object is in, denying (i.e. the code doesn’t compile) the invocation of methods or operations not supported for that specific state. This approach should make it easier to reason about the protocol correctly, without getting lost in an awkward syntax.
Numerous implementations of the actor model now come with built-in support for protocols. An example is Akka, especially with the advent of Akka Typed. Solidity, on the other hand, does not provide any explicit way to model contracts as state machines and to define protocols. To overcome this “limitation”, some languages have been designed supporting typestate-oriented programming. One of the most known is Obsidian, which was specifically designed to provide stronger type-safety guarantees and allow for the specification of protocols.
The interaction contract-oracle could benefit from the introduction of protocols: contracts could ignore all the method calls that would modify their state, invalidating the prerequisites assumed by the oracles.
It might be too late for Solidity to pivot, but the ideas behind Obsidian could help other blockchains use protocols-oriented smart contracts.
- A Concurrent Perspective on Smart Contracts, Ilya Sergey and Aquinas Hobor, https://arxiv.org/pdf/1702.05511.pdf
- Actor Factor, Brooklyn Zelenka, https://medium.com/spadebuilders/actor-factor-2b0005fde786
- A Universal Modular Actor Formalism for Artificial Intelligence, Carl Hewitt, Peter Bishop, and Richard Steiger, https://www.ijcai.org/Proceedings/73/Papers/027B.pdf
- Obsidian, Typestate and Assets for Safer Blockchain Programming, https://arxiv.org/pdf/1909.03523.pdf