Smart contract as an actor
In previous chapters, we talked about the actor model and how it is implemented in the blockchain. Now it is time to look closer into the typical contract structure to understand how different features of the actor model are mapped to it.
This will not be a step-by-step guide on contract creation, as it is a topic for the series itself. It would be going through contract elements roughly to visualize how to handle architecture in the actor model.
The state
As before we would start with the state. Previously we were working with
the cw4-group
contract, so let's start by looking at its code. Go to
cw-plus/contracts/cw4-group/src
. The folder structure should look like
this:
src
├── contract.rs
├── error.rs
├── helpers.rs
├── lib.rs
├── msg.rs
└── state.rs
As you may already figure out, we want to check the state.rs
first.
The most important thing here is a couple of constants: ADMIN
, HOOKS
,
TOTAL
, and MEMBERS
. Every one of such constants represents a single portion
of the contract state - as tables in databases. The types of those constants
represent what kind of table this is. The most basic ones are Item<T>
, which
keeps zero or one element of a given type, and Map<K, T>
which is a key-value
map.
You can see Item
is used to keep an admin and some other data: HOOKS
, and
TOTAL
. HOOKS
is used by the cw4-group
to allow subscription to any
changes to a group - a contract can be added as a hook, so when the group
changes, a message is sent to it. The TOTAL
is just a sum of all members'
weights.
The MEMBERS
in the group contract is the SnapshotMap
- as you can imagine,
it is a Map
, with some steroids - this particular one, gives us access to the
state of the map at some point in history, accessing it by the blockchain
height
. height
is the count of blocks created since the beggining of
blockchain, and it is the most atomic time representation in smart contracts.
There is a way to access the clock time in them, but everything happening in a
single block is considered happening in the same moment.
Other types of storage objects not used in group contracts are:
IndexedMap
- another map type, that allows accessing values by a variety of keysIndexedSnapshotMap
-IndexedMap
andSnapshotMap
married
What is very important - every state type in the contract is accessed using
some name. All of those types are not containers, just accessors to the state.
Do you remember that I told you before that blockchain is our database? And
that is correct! All those types are just ORM to this database - when we use
them to get actual data from it, we pass a special State
object to them, so
they can retrieve items from it.
You may ask - why all that data for a contract are not auto-fetched by whatever is running it. That is a good question. The reason is that we want contracts to be lazy with fetching. Copying data is a very expensive operation, and for everything happening on it, someone has to pay - it is realized by gas cost. I told you before, that as a contract developer you don't need to worry about gas at all, but it was only partially true. You don't need to know exactly how gas is calculated, but by lowering your gas cost, you would may execution of your contracts cheaper which is typically a good thing. One good practice to achieve that is to avoid fetching data you will not use in a particular call.
Messages
In a blockchain, contracts communicate with each other by some JSON
messages. They are defined in most contracts in the msg.rs
file. Take
a look at it.
There are three types on it, let's go through them one by one.
The first one is an InstantiateMsg
. This is the one, that is sent
on contract instantiation. It typically contains some data which
is needed to properly initialize it. In most cases, it is just a
simple structure.
Then there are two enums: ExecuteMsg
, and QueryMsg
. They are
enums because every single variant of them represents a different
message which can be sent. For example, the ExecuteMsg::UpdateAdmin
corresponds to the update_admin
message we were sending previously.
Note, that all the messages are attributed with
#[derive(Serialize, Deserialize)]
, and
#[serde(rename_all="snake_case")]
. Those attributes come from
the serde crate, and they help us with
deserialization of them (and serialization in case of sending
them to other contracts). The second one is not required,
but it allows us to keep a camel-case style in our Rust code,
and yet still have JSONs encoded with a snake-case style more
typical to this format.
I encourage you to take a closer look at the serde
documentation,
like everything there, can be used with the messages.
One important thing to notice - empty variants of those enums,
tend to use the empty brackets, like Admin {}
instead of
more Rusty Admin
. It is on purpose, to make JSONs cleaner,
and it is related to how serde
serializes enum.
Also worth noting is that those message types are not set in stone,
they can be anything. This is just a convention, but sometimes
you would see things like ExecuteCw4Msg
, or similar. Just keep
in mind, to keep your message name obvious in terms of their
purpose - sticking to ExecuteMsg
/QueryMsg
is generally a good
idea.
Entry points
So now, when we have our contract message, we need a way to handle
them. They are sent to our contract via entry points. There are
three entry points in the cw4-group
contract:
#![allow(unused)] fn main() { #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, env: Env, _info: MessageInfo, msg: InstantiateMsg, ) -> Result<Response, ContractError> { // ... } #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result<Response, ContractError> { // .. } #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> { // .. } }
Those functions are called by the CosmWasm virtual machine when
a message is to be handled by contract. You can think about them
as the main
function of normal programs, except they have a signature
that better describes the blockchain itself.
What is very important is that the names of those entry points (similarly to
the main
function) are fixed - it is relevant, so the virtual machine knows
exactly what to call.
So, let's start with the first line. Every entry point is attributed with
#[cfg_attr(not(feature = "library"), entry_point)]
. It may look a bit
scary, but it is just a conditional equivalent of #[entry_point]
-
the attribute would be there if and only if the "library" feature is not set.
We do this to be able to use our contracts as dependencies for other
contracts - the final binary can contain only one copy of each entry point,
so we make sure, that only the top-level one is compiled without this
feature.
The entry_point
attribute is a macro that generates some boilerplate.
As the binary is run by WASM virtual machine, it doesn't know much about
Rust types - the actual entry point signatures are very inconvenient to
use. To overcome this issue, there is a macro created, which generates
entry points for us, and those entry points are just calling our functions.
Now take a look at functions arguments. Every single entry point takes as
the last argument a message which triggered the execution of it (except for
reply
- I will explain it later). In addition to that, there are
additional arguments provided by blockchain:
Deps
orDepsMut
object is the gateway to the world outside the smart contract context. It allows accessing the contract state, as well as querying other contracts, and also delivers anApi
object with a couple of useful utility functions. The difference is thatDepsMut
allows updating state, whileDeps
allows only to look at it.Env
object delivers information about the blockchain state at the moment of execution - its height, the timestamp of execution and information about the executing contract itself.MessageInfo
object is information about the contract call - it contains the address which sends the message, and the funds sent with the message.
Keep in mind, that the signatures of those functions are fixed (except
the messages type), so you cannot interchange Deps
with DepsMut
to
update the contract state in the query call.
The last portion of entry points is the return type. Every entry point returns
a Result
type, with any error which can be turned into a string - in case of
contract failure, the returned error is just logged. In most cases, the error
type is defined for a contract itself, typically using a
thiserror crate. Thiserror
is
not required here, but is strongly recommended - using it makes the error
definition very straightforward, and also improves the testability of the
contract.
The important thing is the Ok
part of Result
. Let's start with the
query
because this one is the simplest. The query always returns the Binary
object on the Ok
case, which would contain just serialized response.
The common way to create it is just calling a to_binary
method
on an object implementing serde::Serialize
, and they are typically
defined in msg.rs
next to message types.
Slightly more complex is the return type returned by any other entry
point - the cosmwasm_std::Response
type. This one keep everything
needed to complete contract execution. There are three chunks of
information in that.
The first one is an events
field. It contains all events, which would
be emitted to the blockchain as a result of the execution. Events have
a really simple structure: they have a type, which is just a string,
and a list of attributes which are just string-string key-value pairs.
You can notice that there is another attributes
field on the Response
.
This is just for convenience - most executions would return
only a single event, and to make it a bit easier to operate one, there
is a set of attributes directly on response. All of them would be converted
to a single wasm
event which would be emitted. Because of that, I consider
events
and attributes
to be the same chunk of data.
Then we have the messages field, of SubMsg
type. This one is the clue
of cross-contact communication. Those messages would be sent to the
contracts after processing. What is important - the whole execution is
not finished, unless the processing of all sub-messages scheduled by the contract
finishes. So, if the group contract sends some messages as a result of
update_members
execution, the execution would be considered done only if
all the messages sent by it would also be handled (even if they failed).
So, when all the sub-messages sent by contract are processed, then all the
attributes generated by all sub-calls and top-level calls are collected and
reported to the blockchain. But there is one additional piece of information -
the data
. So, this is another Binary
field, just like the result of a query
call, and just like it, it typically contains serialized JSON. Every contract
call can return some additional information in any format. You may ask - in
this case, why do we even bother returning attributes? It is because of a
completely different way of emitting events and data. Any attributes emitted by
the contract would be visible on blockchain eventually (unless the whole
message handling fails). So, if your contract emitted some event as a result of
being sub-call of some bigger use case, the event would always be there visible
to everyone. This is not true for data. Every contract call would return only
a single data
chunk, and it has to decide if it would just forward the data
field of one of the sub-calls, or maybe it would construct something by itself.
I would explain it in a bit more detail in a while.
Sending submessages
I don't want to go into details of the Response
API, as it can be read
directly from documentation, but I want to take a bit closer look at the part
about sending messages.
The first function to use here is add_message
, which takes as an argument the
CosmosMsg
(or rather anything convertible to it). A message added to response
this way would be sent and processed, and its execution would not affect the
result of the contract at all.
The other function to use is add_submessage
, taking a SubMsg
argument. It
doesn't differ much from add_message
- SubMsg
just wraps the CosmosMsg
,
adding some info to it: the id
field, and reply_on
. There is also a
gas_limit
thing, but it is not so important - it just causes sub-message
processing to fail early if the gas threshold is reached.
The simple thing is reply_on
- it describes if the reply
message should be
sent on processing success, on failure, or both.
The id
field is an equivalent of the order id in our KFC example from the
very beginning. If you send multiple different sub-messages, it would be
impossible to distinguish them without that field. It would not even be
possible to figure out what type of original message reply handling is! This is
why the id
field is there - sending a sub-message you can set it to any
value, and then on the reply, you can figure out what is happening based on
this field.
An important note here - you don't need to worry about some sophisticated way of generating ids. Remember, that the whole processing is atomic, and only one execution can be in progress at once. In most cases, your contract sends a fixed number of sub-messages on very concrete executions. Because of that, you can hardcode most of those ids while sending (preferably using some constant).
To easily create submessages, instead of setting all the fields separately,
you would typically use helper constructors: SubMsg::reply_on_success
,
SubMsg::reply_on_error
and SubMsg::reply_always
.
CosmosMsg
If you took a look at the CosmosMsg
type, you could be very surprised - there
are so many variants of them, and it is not obvious how they relate to
communication with other contracts.
The message you are looking for is the WasmMsg
(CosmosMsg::Wasm
variant).
This one is very much similar to what we already know - it has a couple of
variants of operation to be performed by contracts: Execute
, but also
Instantiate
(so we can create new contracts in contract executions), and also
Migrate
, UpdateAdmin
, and ClearAdmin
- those are used to manage
migrations (will tell a bit about them at the end of this chapter).
Another interesting message is the BankMsg
(CosmosMsg::Bank
). This one
allows a contract to transfer native tokens to other contracts (or burn them -
equivalent to transferring them to some black whole contract). I like to think
about it as sending a message to a very special contract responsible for handling
native tokens - this is not a true contract, as it is handled by the blockchain
itself, but at least to me it simplifies things.
Other variants of CosmosMsg
are not very interesting for now. The Custom
one is there to allow other CosmWasm-based blockchains to add some
blockchain-handled variant of the message. This is a reason why most
message-related types in CosmWasm are generic over some T
- this is just a
blockchain-specific type of message. We will never use it in the wasmd
. All
other messages are related to advanced CosmWasm features, and I will not
describe them here.
Reply handling
So now that we know how to send a submessage, it is time to talk about handling the reply. When sub-message processing is finished, and it is requested to reply, the contract is called with an entry point:
#![allow(unused)] fn main() { #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> { // ... } }
The DepsMut
, and Env
arguments are already familiar, but there is a new
one, substituting the typical message argument: the cosmwasm_std::Reply
.
This is a type representing the execution status of the sub-message. It is
slightly processed cosmwasm_std::Response
. The first important thing it contains
is an id
- the same, which you set sending sub-message, so now you can
identify your response. The other one is the ContractResult
, which is very
similar to the Rust Result<T, String>
type, except it is there for
serialization purposes. You can easily convert it into a Result
with an
into_result
function.
In the error case of ContracResult
, there is a string - as I mentioned
before, errors are converted to strings right after execution. The Ok
case
contains SubMsgExecutionResponse
with two fields: events
emitted by
sub-call, and the data
field embedded on response.
As said before, you never need to worry about forwarding events - CosmWasm
would do it anyway. The data
however, is another story. As mentioned before,
every call would return only a single data object. In the case of sending
sub-messages and not capturing a reply, it would always be whatever is returned
by the top-level message. But it is not the case when reply
is called. If a
a reply is called, then it is a function deciding about the final data
. It can
decide to either forward the data from the sub-message (by returning None
) or
to overwrite it. It cannot choose, to return data from the original execution
processing - if the contract sends sub-messages waiting for replies, it is
supposed to not return any data, unless replies are called.
But what happens if multiple sub-messages are sent? What would the final
data
contain? The rule is - the last non-None. All sub-messages are always
called in the order of adding them to the Response
. As the order is
deterministic and well defined, it is always easy to predict which reply would
be used.
Migrations
I mentioned migrations earlier when describing the WasmMsg
. So, migration
is another action possible to be performed by contracts, which is kind
of similar to instantiate. In software engineering, it is a common thing to
release an updated version of applications. It is also a case in the blockchain -
SmartContract can be updated with some new features. In such cases, a new
code is uploaded, and the contract is migrated - so it knows that from
this point, its messages are handled by another, updated contract code.
However, it may be that the contract state used by the older version of the contract differs from the new one. It is not a problem if some info was added (for example some additional map - it would be just empty right after migration). But the problem is, when the state changes, for example, the field is renamed. In such a case, every contract execution would fail because of (de)serialization problems. Or even more subtle cases, like adding a map, but one which should be synchronized with the whole contract state, not empty.
This is the purpose of the migration
entry point. It looks like this:
#![allow(unused)] fn main() { #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response<T>, ContracError> { // .. } }
MigrateMsg
is the type defined by the contract in msg.rs
.
The migrate
entry point would be called at the moment of performing
the migration, and it is responsible for making sure the state is correct
after the migration. It is very similar to schema migrations in traditional
database applications. And it is also kind of difficult, because of version
management involved - you can never assume, that you are migrating a contract
from the previous version - it can be migrated from any version, released
anytime - even later than that version we are migrating to!
It is worth bringing back one issue from the past - the contract admin. Do you
remember the --no-admin
flag we set previously on every contract
instantiation? It made our contract unmigrateable. Migrations can be performed
only by contract admin. To be able to use it, you should pass --admin address
flag instead, with the address
being the address that would be able to
perform migrations.
Sudo
Sudo is the last basic entry point in CosmWasm
, and it is the one we would
never use in wasmd
. It is equivalent to CosmosMsg::Custom
, but instead of
being a special blockchain-specific message to be sent and handled by a
blockchain itself, it is now a special blockchain-specific message sent by the
blockchain to contract in some conditions. There are many uses for those, but I
will not cover them, because would not be related to CosmWasm
itself. The
signature of sudo
looks like this:
#![allow(unused)] fn main() { #[cfg_attr(not(feature = "library"), entry_point)] pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> Result<Response, ContractError> { // .. } }
The important difference is that because sudo
messages are blockchain
specific, the SudoMsg
type is typically defined by some blockchain helper
crate, not the contract itself.