An Example Substrate Runtime Module
Substrate is a framework for making custom blockchains, made available by Berlin based Parity Technologies, who are until now the best known for making the second-most popular (after the official one, Go Ethereum) client for the Ethereum blockchain, also called Parity. Parity Technologies is led by Gavin Wood, one of the inventors of Ethereum and as such one of the true authorities in the blockchain space.
Based on their significant experience in developing the Ethereum blockchain, Parity devised Substrate as a framework/toolkit for those who wish to design their own blockchains as opposed to having to build on top of e.g. Ethereum, while not having to reinvent all the basic building blocks, such as consensus logic. Like the Parity client, Substrate is written in the Rust language.
As I have a strong interest in getting to grips with the Rust programming language, and am also curious about blockchain and decentralized technology in general, it made sense for me to investigate Substrate for myself. My resolve to do so strengthened especially after attending their first developer conference, April this year, Sub0.
Runtime Modules
At the conference I attained a basic, birds-eye-view, introduction to Substrate. Importantly, I learnt that a Substrate-based blockchain can be customized with regards to business logic by writing so-called runtime modules, i.e. extensions to the framework’s runtime component. This is a more powerful and robust alternative to writing smart contracts, which is how you would have to introduce custom logic with Ethereum. You can still write smart contracts when building on Substrate, through the Rust based ink! embedded domain specific language, but it will be in the layer above the runtime.
After attending a workshop during the conference, where we learnt to create a CryptoKitties inspired game, the aptly named SubstrateKitties, as a Substrate runtime module, I was armed with the necessary knowledge to be able to make runtime modules myself.
Primarily, I learnt that runtime modules are written in what is almost a domain specific language (DSL), even though it’s standard Rust, thanks to heavy use of Rust procedural macros provided by the framework. Plugging your code into these procedural macros takes time to get used to, but I think it’s a good way of ensuring you don’t shoot yourself in the foot and requiring as little boilerplate code as possible.
I also learnt that Substrate provides the so-called Substrate Runtime Module Library which both provides standard modules for composing the runtime and support macros to aid you in writing custom modules.
My First Runtime Module
In order to gain a proper understanding of Substrate, I decided to write a demo runtime module for myself. First off, I had to choose a topic for it and got the idea of modeling arranging of (coding) workshops. This fell naturally, since I like going to workshops myself and am interested in arranging them as part of community building. The module I designed is just an example, it should be pointed out, but I might make something more substantial around this theme in the future.
The source code for this example module is on GitLab. The majority of the project consists of boilerplate generated code, and all the custom logic is in runtime/src/workshop.rs. I will walk you through the various parts of the code in the following sections.
AnnouncedWorkshop
At the heart of the module is the AnnouncedWorkshop
struct. It derives from balances::Trait
,
so that we can use the balances
module, which enables us to deal with account balances.
pub trait Trait: balances::Trait {
/// The overarching event type.
type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
}
#[derive(Encode, Decode, Default, Clone, PartialEq, Debug)]
pub struct AnnouncedWorkshop<T: Trait> {
organizer: T::AccountId,
min_participants: u32,
fee: T::Balance,
leader: T::AccountId,
leader_fee: T::Balance,
}
Storage
In order to enable storing of state to the blockchain, we must use
the decl_storage
macro, with a trait Store
definition. Within this trait, we declare the different
data fields that we need to store:
-
Budget
-
Workshop
-
Participants/NumParticipants (in Substrate modules, one has to model maps as arrays paired with counters)
decl_storage! {
trait Store for Module<T: Trait> as Workshop {
Budget get(budget): T::Balance;
Workshop get(announced): Option<AnnouncedWorkshop<T>>;
// Lists must be modeled as integer indexed maps combined with a size property
Participants: map u32 => T::AccountId;
NumParticipants get(num_participants): u32;
}
}
Business Logic
The module’s business logic itself is defined within an invocation of the decl_module
macro, as the struct Module>
. It contains a number of public methods that can be invoked
by issuing transactions/https://substrate.dev/docs/en/overview/extrinsics[extrinsics] against
the blockchain.
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
// Initializing events
fn deposit_event<T>() = default;
/// Announce an impending workshop.
pub fn announce_workshop(origin, leader: T::AccountId, leader_fee: T::Balance,
min_participants: u32, fee: T::Balance) -> Result {
let sender = ensure_signed(origin)?;
ensure!(min_participants > 0, "there must be at least one participant");
match <Workshop<T>>::get() {
Some(_) => {
Err("a workshop is already underway")
}
None => {
<Workshop<T>>::put(AnnouncedWorkshop{
min_participants: min_participants, fee: fee, leader: leader.clone(),
leader_fee: leader_fee, organizer: sender.clone(),
});
Self::deposit_event(RawEvent::WorkshopAnnounced(sender, leader));
Ok(())
}
}
}
/// Participate in the currently announced workshop.
/// The workshop fee is deducted from the sender's account.
pub fn participate(origin) -> Result {
let sender = ensure_signed(origin)?;
let workshop = match <Workshop<T>>::get() {
Some(workshop) => {
Ok(workshop)
}
None => {
Err("no workshop announced")
}
}?;
let mut num_participants = <NumParticipants<T>>::get();
// Check if sender already participates in event
for i in 0..num_participants {
let acc_id = <Participants<T>>::get(i);
if acc_id == sender {
return Ok(());
}
}
let _ = <balances::Module<T> as Currency<_>>::withdraw(&sender, workshop.fee,
WithdrawReason::Reserve, ExistenceRequirement::KeepAlive)?;
<Participants<T>>::insert(num_participants, &sender);
num_participants = num_participants.saturating_add(1);
<NumParticipants<T>>::put(num_participants);
let mut budget = <Budget<T>>::get();
budget = budget.saturating_add(workshop.fee);
<Budget<T>>::put(budget);
Ok(())
}
/// Arrange the currently announced workshop.
pub fn arrange_workshop(origin) -> Result {
let sender = ensure_signed(origin)?;
let workshop = match <Workshop<T>>::get() {
Some(workshop) => {
Ok(workshop)
}
None => {
Err("no workshop announced")
}
}?;
ensure!(sender == workshop.organizer, "you must be the organizer");
let num_participants = <NumParticipants<T>>::get();
ensure!(num_participants >= workshop.min_participants, "too few participants");
let mut budget = <Budget<T>>::get();
budget = budget.saturating_sub(workshop.leader_fee);
let _ = <balances::Module<T> as Currency<_>>::deposit_creating(
&workshop.leader, workshop.leader_fee);
let _ = <balances::Module<T> as Currency<_>>::deposit_creating(
&workshop.organizer, budget);
Self::reset_workshop();
Self::deposit_event(RawEvent::WorkshopHeld(workshop.organizer,
num_participants));
Ok(())
}
/// Cancel the currently announced workshop
pub fn cancel_workshop(origin) -> Result {
let sender = ensure_signed(origin)?;
let workshop = match <Workshop<T>>::get() {
Some(workshop) => {
Ok(workshop)
}
None => {
Err("no workshop announced")
}
}?;
ensure!(sender == workshop.organizer, "you must be the organizer");
let num_participants = <NumParticipants<T>>::get();
for i in 0..num_participants {
let participant = <Participants<T>>::get(i);
let _ = <balances::Module<T> as Currency<_>>::deposit_into_existing(
&participant, workshop.fee)?;
}
Self::reset_workshop();
Self::deposit_event(RawEvent::WorkshopCanceled(workshop.organizer));
Ok(())
}
}
}
Announcing a Workshop
The announce_worshop
method allows the transaction sender to announce that a workshop is to
be held, with a certain leader, a certain participation fee and a certain minimum amount of
participants. If a workshop has already been announced, the method will return an error.
If the transaction is found to be valid, an AnnouncedWorkshop
object gets stored with the
provided parameters, and the WorkshopAnnounced
event gets fired.
Participating in a Workshop
The participate
method allows the transaction sender to sign up for the currently announced
workshop. If no error is detected, for example that no workshop is announced, the announced
fee is deducted from the sender’s balance and the sender is registered among the workshop’s
participants. The participant fee is also added to the workshop budget.
Arranging a Workshop
The arrange_workshop
method lets the sender register that the currently announced workshop
has been arranged. If the sender is not the organizer, the method will fail. Providing
that no errors are detected, the workshop leader’s fee gets added to their balance and
the remainder of the budget gets added to the balance of the organizer. Further, the
announced workshop gets reset and the WorkshopHeld
gets fired.
Canceling a Workshop
The cancel_workshop
method lets the sender register that the currently announced workshop
was canceled. If the sender is not the organizer, the method will fail. Providing
that no errors are detected, the participants' fees get refunded and the currently
announced workshop reset. The WorkshopCanceled
gets fired.
Events
My runtime module includes three events:
-
WorkshopAnnounced
-
WorkshopHeld
-
WorkshopCanceled
These get defined through an enum
type in an invocation of the decl_event
macro:
decl_event!(
pub enum Event<T> where AccountId = <T as system::Trait>::AccountId {
WorkshopAnnounced(AccountId, AccountId),
WorkshopHeld(AccountId, u32),
WorkshopCanceled(AccountId),
}
);
Conclusion
I hope this was helpful in understanding how to write runtime modules for Substrate!