Understanding Substrate Runtime Development and Pallets
The Substrate runtime is the core logic of a blockchain, written in Rust and compiled to WebAssembly. It defines the state transition function, handles transactions, and manages the blockchain's state. Understanding runtime development is essential for building custom blockchains.
A Substrate runtime consists of several key components:
FRAME is the framework used to build Substrate runtimes. It provides a modular approach to runtime development:
Pallets are the building blocks of a Substrate runtime. Each pallet provides specific functionality:
Core pallets that provide essential blockchain functionality.
Pallets that handle consensus and block production.
Pallets that provide additional functionality.
Configuring a Substrate runtime involves defining pallets and their parameters:
// Runtime configuration example
use frame_support::{
construct_runtime, parameter_types,
traits::{Everything, OnInitialize, OnFinalize},
weights::Weight,
};
// Define the runtime
construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system,
Timestamp: pallet_timestamp,
Balances: pallet_balances,
TransactionPayment: pallet_transaction_payment,
Sudo: pallet_sudo,
// Custom pallets
Template: pallet_template,
}
);
// Configure system pallet
impl frame_system::Config for Runtime {
type BaseCallFilter = Everything;
type BlockWeights = BlockWeights;
type BlockLength = BlockLength;
type DbWeight = RocksDbWeight;
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Nonce = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = AccountId;
type Lookup = AccountIdLookup<AccountId, ()>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = BlockHashCount;
type Version = Version;
type PalletInfo = PalletInfo;
type AccountData = AccountData<Balance>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = SS58Prefix;
type OnSetCode = ();
type MaxConsumers = ConstU32<16>;
}
// Configure balances pallet
impl pallet_balances::Config for Runtime {
type MaxLocks = ConstU32<50>;
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = Balance;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type WeightInfo = pallet_balances::weights::SubstrateWeight<Runtime>;
type FreezeIdentifier = ();
type MaxFreezes = ();
type HoldIdentifier = ();
type MaxHolds = ();
}Creating custom pallets allows you to add specific functionality to your runtime:
// Custom pallet example
use frame_support::{
decl_module, decl_storage, decl_event, decl_error,
traits::{Get, Randomness},
weights::Weight,
};
// Define the pallet
decl_storage! {
trait Store for Module<T: Config> as TemplateModule {
// Storage items
pub Something get(fn something): Option<u32>;
pub Nonce get(fn nonce): u64;
}
}
// Define events
decl_event!(
pub enum Event<T> where AccountId = <T as frame_system::Config>::AccountId {
SomethingStored(u32, AccountId),
}
);
// Define errors
decl_error! {
pub enum Error for Module<T: Config> {
NoneValue,
StorageOverflow,
}
}
// Define the module
decl_module! {
pub struct Module<T: Config> for enum Call where origin: T::RuntimeOrigin {
type Error = Error<T>;
type RuntimeEvent = Event<T>;
// Initialize the module
fn on_initialize(_n: T::BlockNumber) -> Weight {
Weight::from_parts(10_000, 0)
}
// Finalize the module
fn on_finalize(_n: T::BlockNumber) {
// Clean up
}
// Dispatchable function
#[weight = 10_000]
pub fn do_something(origin, something: u32) -> DispatchResult {
let who = ensure_signed(origin)?;
// Validate input
ensure!(something > 0, Error::<T>::NoneValue);
// Update storage
<Something<T>>::put(something);
<Nonce<T>>::mutate(|n| *n += 1);
// Emit event
Self::deposit_event(Event::SomethingStored(something, who));
Ok(())
}
}
}
// Define the configuration trait
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}Runtime APIs provide a way for external clients to interact with the runtime:
// Runtime API example
use sp_api::decl_runtime_apis;
// Define the runtime API
decl_runtime_apis! {
/// API for getting the current block number
pub trait BlockNumberApi {
/// Get the current block number
fn current_block_number() -> u64;
}
/// API for getting random data
pub trait RandomnessApi {
/// Get random data
fn random_data() -> [u8; 32];
}
}
// Implement the runtime API
impl sp_api::Core<Block> for Runtime {
fn version() -> RuntimeVersion {
VERSION
}
fn execute_block(block: Block) {
Executive::execute_block(block);
}
}
impl sp_api::Metadata<Block> for Runtime {
fn metadata() -> OpaqueMetadata {
OpaqueMetadata::new(Runtime::metadata().into())
}
}
impl sp_block_builder::BlockBuilder<Block> for Runtime {
fn apply_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> ApplyExtrinsicResult {
Executive::apply_extrinsic(extrinsic)
}
fn finalize_block() -> <Block as BlockT>::Header {
Executive::finalize_block()
}
fn inherent_extrinsics(data: sp_inherents::InherentData) -> Vec<<Block as BlockT>::Extrinsic> {
data.create_extrinsics()
}
fn check_inherents(block: Block, data: sp_inherents::InherentData) -> sp_inherents::CheckInherentsResult {
data.check_extrinsics(&block)
}
}Substrate supports runtime upgrades without hard forks:
Testing is crucial for runtime development:
// Runtime testing example
use frame_support::{
assert_ok, assert_noop,
traits::{OnInitialize, OnFinalize},
};
#[test]
fn test_do_something() {
new_test_ext().execute_with(|| {
// Test initial state
assert_eq!(TemplateModule::something(), None);
// Test successful execution
assert_ok!(TemplateModule::do_something(Origin::signed(1), 42));
assert_eq!(TemplateModule::something(), Some(42));
// Test error case
assert_noop!(
TemplateModule::do_something(Origin::signed(1), 0),
Error::<Test>::NoneValue
);
});
}
#[test]
fn test_on_initialize() {
new_test_ext().execute_with(|| {
// Test initialization
TemplateModule::on_initialize(1);
// Add assertions here
});
}
#[test]
fn test_on_finalize() {
new_test_ext().execute_with(|| {
// Test finalization
TemplateModule::on_finalize(1);
// Add assertions here
});
}In this chapter, we've explored Substrate runtime development:
In the final chapter, we'll put everything together and learn how to build a complete Substrate-based blockchain from scratch.