Exploring smart contracts as actors


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.”
Carl Hewitt

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);

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;

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 public;

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:

  1. the initial state is Close;

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.