Idea behind an Actor Model
The actor model is the solution to the problem of communication between smart contracts. Let's take a look at the reasons why this particular solution is chosen in CosmWasm, and what are the consequences of that.
The problem
Smart contracts can be imagined as sandboxed microservices. Due to SOLID principles, it is valuable to split responsibilities between entities. However, to split the work between contracts themselves, there is a need to communicate between them, so if one contract is responsible for managing group membership, it is possible to call its functionality from another contract.
The traditional way to solve this problem in SW engineering is to model services as functions that would be called with some RPC mechanism, and return its result as a response. Even though this approach looks nice, it creates sort of problems, in particular with shared state consistency.
The other approach which is far more popular in business level modelling is to treat entities as actors, which can perform some task, but without interrupting it with calls to other contracts. Any calls to other contracts can be called only after the whole execution is performed. When "subcall" would be finished, it will call the original contract back.
This solution may feel unnatural, and it requires to kind of different design solutions, but it turns out to work pretty well for smart contracts execution. I will try to explain how to reason about it, and how it maps to contract structure step by step.
The Actor
The most important thing in the whole model is an Actor itself. So what is this? The Actor is a single instantiation of a contract, which can perform several actions. When the actor finishes his job, he prepares a summary of it, which includes the list of things that have to be done, to complete the whole scheduled task.
An example of an actor is the Seller in the KFC restaurant. The first thing you do is order your BSmart, so you are requesting action from him. So from the system user, you can think about this task as "sell and prepare my meal", but the action performed by the seller is just "Charge payment and create order". The first part of this operation is to create a bill and charge you for it, and then it requests the Sandwich and Fries to be prepared by other actors, probably chefs. Then when the chef is done with his part of the meal, he checks if all meals are ready. If so, it calls the last actor, the waiter, to deliver the food to you. At this point, you can receive your delivery, and the task is considered complete.
The above-described workflow is kind of simplified. In particular - in a typical restaurant, a waiter would observe the kitchen instead of being triggered by a chef, but in the Actor model, it is not possible. Here, entities of the system are passive and cannot observe the environment actively - they only react to messages from other system participants. Also in KFC, the seller would not schedule subtasks for particular chefs, instead, he would leave tasks to be taken by them, when they are free. It is not the case, because as before - chefs cannot actively listen to the environment. However, it would be possible to create a contract for being a chefs dispatcher which would collect all orders from sellers, and balance them across chefs for some reason.
The Action
Actors are the model entities, but to properly communicate with them, we need some kind of protocol. Every actor is capable of performing several actions. In my previous KFC example, the only action seller can do is "Charge payment and create order". However, it is not always the case - our chefs were proficient with performing both "Prepare fries" and "Prepare Sandwich" actions - and also many more.
So when we want to do something in an actor system, we schedule some action to the actor being the closest to us, very often with some additional parameters (as we can pick if we want to exchange fries with salad).
However, naming the action after the exact thing which happened in the very contract would be misleading. Take a look at the KFC example once again. As I mentioned, the action performed by a seller is "Charge payment and create order". The problem is, that for the client which schedules this action, it doesn't matter what exactly is the responsibility of the actor himself - what the client is scheduling is "Prepare Meal" with some description of what exactly to prepare. So we can say, that the action is the thing performed by the contract itself, plus all the sub-actions it schedules.
Multi-stage Actions
So as the whole idea makes some sense, there is the problem created by the actor model: what if I want to perform some action in my contract, but to completely finalize some steps, the contract has to make sure that some sub-action he scheduled are finished?
Imagine, that in the previous KFC situation there is no dedicated Waiter, instead the Seller is serving you a meal when the Chefs finished their job.
This kind of pattern is so important and common, that in CosmWasm
we developed a special way to handle it, which is dedicated Reply
action.
So when Seller is scheduling actions for chefs, he assigns some
number to this action (like order id) and passes it to chefs.
He also remembers how many actions he scheduled for every order id.
Now every time chef is finished with his action, he would call the
special Reply
action on Seller, in which he would pass back the
order id. Then Seller would decrease the number of actions left for
this order, and if it reached zero, he would serve a meal.
Now you can say, that the Reply
action is completely not needed,
as Chefs could just schedule any arbitrary action on Seller, like
Serve
, why there is the special Reply
for? The reason is
abstraction and reusability. The Chefs task is to prepare a meal,
and that is all. There is no reason for him to know why he is
even preparing Fries - if it is part of the bigger task (like order
for a client), or the seller is just hungry. It is possible,
that not only the seller is eligible to call the chef for food - possibly
any restaurant employee can do that just for themselves. Therefore,
we need a way, to be able to react to an actor finishing his job in
some universal way, to handle this situation properly in any context.
It is worth noting, that the Reply
can contain some additional data.
The id assigned previously is the only required information in the
Reply
call, but the actor can pass some additional data - events
emitted, which are mostly metadata (to be observed by non-blockchain
applications mostly), and any arbitrary data it wants to pass.
State
Up until this point, we were considering actors as entities performing some job, like preparing the meal. If we are considering computer programs, such a job would be to show something on the screen, maybe print something. This is not the case with Smart Contracts. The only thing which can be affected by the Smart Contract is their internal state. So the state is arbitrary data that is kept by the contract. Previously in the KFC example I mentioned, that Seller is keeping in mind how many actions he scheduled for chefs are not yet finished - this number is part of the Seller's state.
To give a more realistic example of a contract state, let's think about a more
real-life Smart Contract than the restaurant. Let's imagine we want
to create our currency - maybe we want to create some smart
contracts-based market for some MMORPG game. So we need some way,
to be able to at least transfer currency between players. We can
do that, by creating the contract we would call MmoCurrency
,
which would support the Transfer
action to transfer money to another
player. Then what would be the state of such a contract? It would be
just a table mapping player names to the amount of currency they own. The
contract we just invented exists in CosmWasm examples, and it
is called the
cw20-base
contract
(it is a bit more complicated, but
it is its core idea).
And now there is a question - how is this helpful to transfer currency if I cannot check how much of it I own? It is a very good question, and the answer to that is simple - the whole state of every contract in our system is public. It is not universal for every Actor model, but it is how it works in CosmWasm, and it is kind of forced by the nature of blockchain. Everything happening in blockchain has to be public, and if some information should be hidden, it has to be stored indirectly.
There is one very important thing about the state in CosmWasm, and it is
the state being transactional. Any updates to the state are not applied immediately,
but only when the whole action succeeds. It is very important, as
it guarantees that if something goes wrong in the contract, it is always
left in some proper state. Let's consider our MmoCurrency
case. Imagine,
that in the Transfer
action we first increase the receiver currency amount
(by updating the state), and only then do we decrease the sender amount.
However, before decreasing it, we need to check if a sender
possesses enough funds to perform the transaction. In case we realize,
that we cannot do it, we don't need to do any rolling back by hand -
we would just return a failure from the action execution, and the state
would not be updated. So when in contract state is updated,
it is just a local copy of this state being altered, but the
partial changes would never be visible by other contracts.
Queries
There is one building block in the CosmWasm approach to the Actor model, which I didn't yet cover. As I said, the whole state of every contract is public and available for everyone to look at. The problem is, that this way of looking at state is not very convenient - it requires users of contracts to know its internal structure, which kind of violates the SOLID rules (Liskov substitution principle in particular). If for example contract is updated and its state structure changes a bit, another contract looking at its state would just nevermore work. Also, it is often the case, that the contract state is kind of simplified, and information that is relevant to the observer would be calculated from the state.
This is where queries come into play. Queries are the type of messages to contract, which does not perform any actions, so do not update any state, but can return an answer immediately.
In our KFC comparison, the query would be if Seller goes to Chef to ask "Do we still have pickles available for our cheeseburgers"? It can be done while operating, and response can be used in it. It is possible because queries can never update their state, so they do not need to be handled in a transactional manner.
However, the existence of queries doesn't mean, that we cannot look at the contract's
state directly - the state is still public, and the technique of looking at them
directly is called Raw Queries
. For clarity, non-raw queries are sometimes
denoted as Smart Queries
.
Wrapping everything together - transactional call flow
So we touched on many things here, and I know it may be kind of confusing. Because of that, I would like to go through some more complicated calls to CosmWasm contract to visualize what the "transactional state" means.
Let's imagine two contracts:
- The
MmoCurrency
contract mentioned before, which can performTransfer
action, allows transferring someamount
of currency to somereceiver
. - The
WarriorNpc
contract, which would have some amount of our currency, and he would be used by our MMO engine to pay the reward out for some quest player could perform. It would be triggered byPayout
action, which can be called only by a specific client (which would be our game engine).
Now here is an interesting thing - this model forces us, to make our MMO
more realistic in terms of the economy that we traditionally see - it is
because WarriorNpc
has some amount of currency, and cannot create
more out of anything. It is not always the case (the previously mentioned
cw20
has a notion of Minting for this case), but for sake of simplicity
let's assume this is what we want.
To make the quest reasonable for longer, we would make a reward for it
to be always between 1 mmo
and 100 mmo
, but it would be ideally
15%
of what Warrior owns. This means, that the quest reward decreases
for every subsequent player, until Warrior would be broke, left
with nothing, and will no longer be able to payout players.
So what would the flow look like? The first game would send a Payout
message
to the WarriorNpc
contract, with info on who should get the reward. Warrior
would keep track of players who fulfilled the quest, to not pay out
the same person twice - there would be a list of players in his state.
First, he would check the list looking for players to pay out - if he is
there, he would finish the transaction with an error.
However, in most cases the player would not be on the list - so then WarriorNpc
would add him to the list. Now the Warrior would finish his part of
the task, and schedule the Transfer
action to be performed by MmoCurrency
.
But there is the important thing - because Transfer
action is actually
the part of the bigger Payout
flow, it would not be executed on the original
blockchain state, but on the local copy of it, which the player's list is already
applied to. So if the MmoCurrency
would for any reason takes a look
at WarriorNpc
internal list, it would be already updated.
Now MmoCurrency
is doing its job, updating the state of Warrior and
player balance (note, that our Warrior is here just treated as another player!).
When it finishes, two things may happen:
- There was an error - possibly Warrior is out of cash, and it can nevermore pay for the task. In such case, none of the changes - neither updating the list of players succeeding, nor balance changes are not applied to the original blockchain storage, so they are like never happened. In the database world, it is denoted as rolling back the transaction.
- Operation succeed - all changes on the state are now applied to blockchain,
and any further observation of
MmoCurrency
orWarriorNpc
by external the world would see updated data.
There is one problem - in this model, our list is not a list of players who fulfilled the quest (as we wanted it to be), but the list of players paid out (as in transfer failure, the list is not updated). We can do better.
Different ways of handling responses
Note, that we didn't mention a Reply
operation at all. So why it was not
called by MmoCurrency
on WarriorNpc
? The reason is, that this operation
is optional. When scheduling sub-actions on another contract we may choose
when Reply
how the result should be handled:
- Never call
Reply
, action fails if sub-message fails - Call
Reply
on success - Call
Reply
on failure - Always call
Reply
So if we do not request Reply
to be called by subsequent contract, it will
not happen. In such a case if a sub-call fails, the whole transaction is rolled
back - sub-message failure transitively causes the original message failure.
It is probably a bit complicated for now, but I promise it would be simple when
you would do some practice with that.
When handling the reply, it is important to remember, that although changes are not yet applied to the blockchain (the transaction still can be failed), the reply handler is already working on the copy of the state with all changes made by sub-message so far applied. In most cases, it would be a good thing, but it has a tricky consequence - if the contract is calling itself recursively, it is possible that subsequent call overwrote things set up in the original message. It rarely happens, but may need special treatment in some cases - for now I don't want to go deeply into details, but I want you to remember about what to expect after state in the actor's flow.
Now let's take a look at handling results with 2
-4
options. It is actually
interesting, that using 2
, even if the transaction is performed by sub-call
succeed, we may now take a look at the data it returned with Reply
, and
on its final state after it finished, and we can still decide,
that action as a whole is a failure, in which case everything would
be rolled back - even currency transfer performed by external contract.
In our case, an interesting option is 3
. So if the contract would call
Reply
on failure, we can decide to claim success, and commit a
transaction on the state if the sub call failed. Why it may be relevant
for us? Possibly because our internal list was supposed to keep the list of
players succeeding with the quest, not paid out! So if we have no more currency,
we still want to update the list!
The most common way to use the replies (option 2
in particular) is to
instantiate another contract, managed by the called one. The idea is, that
in those use cases, the creator contract wants to keep the address of created
contract in its state. To do so it has to create an Instantiate
sub-message,
and subscribe for its success response, which contains an address of freshly
created contract.
In the end, you can see, that performing actions in CosmWasm is built with hierarchical state change transactions. The sub-transaction can be applied to the blockchain only if everything succeeds, but in case that sub-transaction failed, only its part may be rolled back, end other changes may be applied. It is very similar to how most database systems work.
Conclusion
Now you have seen the power of the actor model to avoid reentrancy, properly
handle errors, and safely sandbox contracts. This helps us provide the solid
security guarantees of the CosmWasm platform. Let’s get started playing around
with real contracts in the wasmd
blockchain.