Piecrust vs Rusk VM 1.0
After a taste of how a counter example smart contract looks in Piecrust, it is worth to have a look at a functionally equivalent example written for Rusk-VM Version 1.0. An example looks as follows:
As you can see, the sample is much harder to follow and contains much more boilerplate code, including code for arguments passing, deserialization, serialization of return values, special derivations for data structures and more. You may wonder how was persistence implemented in Version 1.0, as there are no special calls related to persistence in this version either. In Version 1.0, contract state is passed in its entirety into each state changing method as an extra first parameter, in a similar manner to how object-oriented languages pass instance references to method calls. After state alteration, a state changing method returns the new state as a return value. State persistence is thus taken care of by the host. You can appreciate that that was a more heavy-weight and less performant solution.
Eventing
In addition to querying and changing the contract state, Rusk VM smart contract can also send events. Events are (by intention light-weight) objects which hold a topic string as well as an attached piece of data. Events are stored by the host and can be queried by the caller of a method which generated events. If contract has a need to inform a user about some operations or facts encountered during execution, and this information may or may not be consumed by the user - events are an ideal tool for that. Events have the advantage that they are not passed as return values of contracts, and because of that many events can be sent during a single query or transaction execution. Let’s have a look at a small contract which generates events:
Method emit_num
of the above contract generates a number of events, according to the value of its argument. Events do not need to be passed as return value, but rather are stored by the host and can optionally be queried later by the caller. This is a very convenient mechanism for passing lightweight and optional information to the user, and for triggering some actions on the user side.
Feeder
Passing return value from a contract query method via its return value is fine for relatively small values or data structures, yet it is impractical for larger collections. The caller may want to process one collection element at a time, and for such scenario a feeder mechanism can be used and it is usually a better alternative. Feeder passes data via a dedicated data channel called mpsc
from Rust’s standard library (mpsc stands for multiple producer single consumer). As contract writer, you do not need to worry about setting up a mpsc channel, as you can use a provided host method instead. The following example shows a simple contract which utilizes a feeder:
Method feed_num
in the above example uses the host call named feed
in order to pass subsequent values of a collection (in this case simple integers) to a mpsc
communication channel. Caller of this method has a mechanism which allows it to pass an mpsc
channel and can consume values as they arrive from the contract.
Host Functions
Contracts are “almost” regular Rust programs convertible to Web Assembly, which means that they follow the usual non-VM requirements like not using the standard library and not using input/output functions. Contracts also run in a so called “hosted” environment, which means that they have some host services available to them. Among those services there is a set of host functions they are allowed to call. Host functions are always available to contracts. So far we have encountered a few of them, like the following:
- wrap_call()
- emit()
- feed()
There are more host functions available and some of them will be described in this section. For the beginning, we’d like to mention the following host functions:
- owner()
- self_id()
- host_query()
First two of those methods belong to a group of so-called “metadata” methods, named so because they provide some information about the contract itself. Method owner()
provides contract id of the contract’s owner, while method self_id()
provides id of the contract itself. Sample contract utilizing these two host calls might look as follows:
As we can see, the host environment provides also some types, like in this example, ContractId
. The last of the host methods we’d like to mention in this section is host_query()
. Method host_query()
is a universal function which allows contracts to call any function that was registered with the host before the contract was called. Let’s say that we would like to perform hashing on the host side rather than by contract’s code. We can write a hash function and register it with the host, so that subsequently we are able to call it from within a contract. Let’s say our hashing function is as follows:
Our hash
function deserializes an argument in a form of a vector of data, hashes it and places the hash in the same area where input parameter were passed. Return value is the length of a passed return data. Here we can see how much harder it is to write code when helpful host methods like wrap_call
are not available.
Function to be registered as host function needs to be of type HostQuery
, which is defined as follows:
Registration of a host function looks as follows:
After our hash function is registered, we are able to call it from within a contract as follows:
ZK Proof Verification
One of the host functions available for contracts is a function to verify Zero Knowledge (ZK) proofs. A method within a contract performing a proof verification could look as follows:
In this way, contract is able to verify ZK proof without having to perform the verification itself, but rather by delegating the work to the host.
Calling Other Contracts
Contracts are allowed to call other contracts, as in the following example:
Host method call
makes it possible for the contract to call a method of a given another contract (identified by its id). The function accepts contract id, name of the function to be called, and function argument, which in the above example is a unit type (argument is empty). There is also another variant of the host call()
function named call_with_limit()
, which in addition to contract id, method name and method argument, accepts a maximum value of gas to be spent by the given call.
Inserting Debugging Statements
Contracts, being Web Assembly modules, running in a Virtual Machine sandbox, are not allowed to perform any input/output operations. Sometimes it is needed, especially for debugging purposes, for the contract to print a message on the console. For this purpose, a host macro named debug!
has been provided. In the following example, contract’s method issues a debugging statement:
Panicking
Sometimes it is necessary for a contract to panic, especially if some critical check of arguments or state fails and there is no point for the contract to continue its execution and waste valuable resources. Host macro named panic!
is provided for this very purpose. In the following example, contract’s method panics:
Constructor and Init
It is possible to export a special contract method named init()
which can perform contact’s initialization of any kind. Such method will be called automatically when the contract is deployed. The main intention behind method init()
is to allow contracts to initialize their state at a time before the contract is operational and ready to receive calls. Method init()
accepts a single argument of any serializable type. That argument will be passed to the init method by code which performs the deployment of the contract. In the following example, we can see a contract with an init()
method:
Method init()
looks like any contract method, and it could do anything other methods can do, it is not limited to only initializing contract’s state. What is special about this method is the fact that the host will detect if it is exported, and it will call it when when the contract is deployed. Let’s have a look at how the deployment of the such contract could be implemented:
As we can see, method deploy()
accepts an argument of type Into<ContractData>
, so any object convertible to ContractData will be accepted. ContractData, on the other hand, contains a field named constuctor_arg
, which is optional, but when set, will be used as an argument to the init()
method of our contract. In effect, we are able to pass data from deployment code, like a contract deployment tool or a wallet, to contract state. Note that in the above example obligatory argument owner
also had to be provided.