Network Framework Modules¶
Introduction¶
The Substrate framework provides complete and ready-to-use implementations of the main functions a Tanssi network needs to work properly, including cryptography, consensus, governance, and so on. These implementations are fully customizable and could be replaced with custom logic if needed.
When building the Runtime, which defines the state transition rules between two blocks applied to a set of transactions, the intended behavior and features of the blockchain need to be set by determining the rules of the state transition.
To build the Runtime, Substrate provides many built-in modules (also known as pallets) that can be freely used as building blocks to compose and interact with any other custom-made modules, allowing teams to create unique behaviors according to the specific requirements of their Tanssi network.
Built-in Modules¶
When designing and writing the rules of a Tanssi network, the available set of functional modules brings a solution to many of the coding requirements that would otherwise need to be built from scratch.
Here is a list of some of the most popular modules:
- Balances - it provides functions for handling accounts and balances for the Tanssi network native currency
- Assets - it provides functions for handling any type of fungible tokens
- NFTs - it provides functions for dealing with non-fungible tokens
- Democracy - it provides functions to manage and administer general stakeholder voting
- Multisig - it provides functions for multi-signature dispatch
- Recovery - it provides functions to allow users to regain access to their accounts when the private key is lost. This works by granting other accounts the right to sign transactions on behalf of the lost account (note that it is necessary to have previously chosen the authorized accounts)
- Staking - it provides functions to administer staked tokens, support rewarding, slashing, depositing, withdrawing, and so on
In addition to those previously listed, other modules like identity, smart contracts, vesting, and many others that are freely available can speed up the development of the Tanssi network and, consequently, the time to market.
Note
The framework also includes other modules that provide core protocol functionality, such as consensus and low-level data encoding.
Custom-Made Modules¶
Developers creating new modules enjoy complete freedom to express any desired behavior in the core logic of the blockchain, like exposing new transactions, storing sensible information, and validating and enforcing business logic.
As explained in the Architecture article, a module needs to be able to communicate with the core client by exposing and integrating with a very specific API that allows the runtime to expose transactions, access storage, and code and decode information stored on-chain. It also needs to include many other required wiring codes that make the module work in the node.
To improve developer experience when writing modules, Substrate relies heavily on Rust macros. Macros are special instructions that automatically expand to Rust code just before compile-time, allowing modules to keep up to seven times the amount of code out of sight of the developers. This allows developers to focus on the specific functional requirements when writing modules instead of dealing with technicalities and the necessary scaffolding code.
All modules in Substrate, including custom-made ones, implement these attribute macros, of which the first three are mandatory:
#[frame_support::pallet]
- this attribute is the entry point that marks the module as usable in the runtime#[pallet::pallet]
- applied to a structure that is used to retrieve module information easily#[pallet::config]
- is a required attribute to define the configuration for the data types of the module#[pallet::call]
- this macro is used to define functions that will be exposed as transactions, allowing them to be dispatched to the runtime. It is here that the developers add their custom transactions and logic#[pallet::error]
- as transactions may not be successful (insufficient funds, as an error example), and for security reasons, a custom module can never end up throwing an exception, all the possible errors are to be identified and listed in an enum to be returned upon an unsuccessful execution#[pallet::event]
- events can be defined and used as a means to provide more information to the user#[pallet::storage]
- this macro is used to define elements that will be persisted in storage. As resources are scarce in a blockchain, it should be used wisely to store only sensible information
All these macros act as attributes that must be applied to the code just above Rust modules, functions, structures, enums, types, etc., allowing the module to be built and added to the runtime, which, in time, will expose the custom logic to the outer world, as exposed in the following section.
Custom Module Example¶
As an example of a custom module, the following code (not intended for production use) showcases the use of the previously mentioned macros by presenting a simple lottery with minimal functionality, exposing two transactions:
-
buy_ticket - this transaction verifies that the user signing the request has not already bought a ticket and has enough funds to pay for it. If everything is fine, the module transfers the ticket price to a special account and registers the user as a participant for the prize
-
award_prize - this transaction generates a random number to pick the winner from the list of participants. The winner gets the total amount of the funds transferred to the module's special account
#![cfg_attr(not(feature = "std"), no_std)]
/// Learn more about FRAME and the core library of Substrate FRAME pallets:
/// <https://docs.substrate.io/reference/frame-pallets/>
pub use pallet::*;
#[frame_support::pallet(dev_mode)]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::{*, ValueQuery, OptionQuery};
use frame_system::pallet_prelude::*;
use scale_info::prelude::vec::Vec;
use frame_support::
{
sp_runtime::traits::AccountIdConversion,
traits:: {
Currency, ExistenceRequirement, Randomness
},
PalletId,
};
type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
#[pallet::pallet]
pub struct Pallet<T>(_);
/// Configure the module by specifying the parameters and types on which it depends.
#[pallet::config]
pub trait Config: frame_system::Config {
// Event definition
type RuntimeEvent: From<Event<Self>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
// Currency
type Currency: Currency<Self::AccountId>;
// Randomness
type MyRandomness: Randomness<Self::Hash, BlockNumberFor<Self>>;
// Ticket cost
#[pallet::constant]
type TicketCost: Get<BalanceOf<Self>>;
// Maximum number of participants
#[pallet::constant]
type MaxParticipants: Get<u32>;
// Module Id
#[pallet::constant]
type PalletId: Get<PalletId>;
}
// The pallet's runtime storage items.
#[pallet::storage]
#[pallet::getter(fn get_participants)]
pub(super) type Participants<T: Config> = StorageValue<
_,
BoundedVec<T::AccountId, T::MaxParticipants>,
OptionQuery
>;
#[pallet::storage]
#[pallet::getter(fn get_nonce)]
pub(super) type Nonce<T: Config> = StorageValue<
_,
u64,
ValueQuery
>;
// Pallets use events to inform users when important changes are made.
// https://docs.substrate.io/main-docs/build/events-errors/
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Event emitted when a ticket is bought
TicketBought { who: T::AccountId },
/// Event emitted when the prize is awarded
PrizeAwarded { winner: T::AccountId },
/// Event emitted when the prize is to be awarded, but there are no participants
ThereAreNoParticipants,
}
// Errors inform users that something went wrong
#[pallet::error]
pub enum Error<T> {
NotEnoughCurrency,
AccountAlreadyParticipating,
CanNotAddParticipant,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(0)]
pub fn buy_ticket(origin: OriginFor<T>) -> DispatchResult {
// 1. Validates the origin signature
let buyer = ensure_signed(origin)?;
// 2. Checks that the user has enough balance to afford the ticket price
ensure!(
T::Currency::free_balance(&buyer) >= T::TicketCost::get(),
Error::<T>::NotEnoughCurrency
);
// 3. Checks that the user is not already participating
if let Some(participants) = Self::get_participants() {
ensure!(
!participants.contains(&buyer),
Error::<T>::AccountAlreadyParticipating
);
}
// 4. Adds the user as a new participant for the prize
match Self::get_participants() {
Some(mut participants) => {
ensure!(
participants.try_push(buyer.clone()).is_ok(),
Error::<T>::CanNotAddParticipant
);
Participants::<T>::set(Some(participants));
},
None => {
let mut participants = BoundedVec::new();
ensure!(
participants.try_push(buyer.clone()).is_ok(),
Error::<T>::CanNotAddParticipant
);
Participants::<T>::set(Some(participants));
}
};
// 5. Transfers the ticket cost to the module's account
// to be hold until transferred to the winner
T::Currency::transfer(
&buyer,
&Self::get_pallet_account(),
T::TicketCost::get(),
ExistenceRequirement::KeepAlive)?;
// 6. Notify the event
Self::deposit_event(Event::TicketBought { who: buyer });
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(0)]
pub fn award_prize(origin: OriginFor<T>) -> DispatchResult {
// 1. Validates the origin signature
let _who = ensure_root(origin)?;
match Self::get_participants() {
Some(participants) => {
// 2. Gets a random number from the randomness module
let nonce = Self::get_and_increment_nonce();
let (random_seed, _) = T::MyRandomness::random(&nonce);
let random_number = <u32>::decode(&mut random_seed.as_ref())
.expect("secure hashes should always be bigger than u32; qed");
// 3. Selects the winner from the participants lit
let winner_index = random_number as usize % participants.len();
let winner = participants.as_slice().get(winner_index).unwrap();
// 4. Transfers the total prize to the winner's account
let prize = T::Currency::free_balance(&Self::get_pallet_account());
T::Currency::transfer(
&Self::get_pallet_account(),
&winner,
prize,
ExistenceRequirement::AllowDeath)?;
// 5. Resets the participants list, and gets ready for another lottery round
Participants::<T>::kill();
// 6. Notify the event
Self::deposit_event(Event::PrizeAwarded { winner: winner.clone() } );
},
None => {
// Notify the event (No participants)
Self::deposit_event(Event::ThereAreNoParticipants);
}
};
Ok(())
}
}
impl<T: Config> Pallet<T> {
fn get_pallet_account() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
fn get_and_increment_nonce() -> Vec<u8> {
let nonce = Nonce::<T>::get();
Nonce::<T>::put(nonce.wrapping_add(1));
nonce.encode()
}
}
}
For more information about the step-by-step process of creating a custom-made module to the runtime, please refer to the Adding a Custom-Made Module in the Builder's section.
| Created: June 15, 2023