Substrate Runtime

Understanding Substrate Runtime Development and Pallets

Substrate Runtime Architecture

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.

Runtime Components

A Substrate runtime consists of several key components:

Core Components

  • • System pallet
  • • Consensus logic
  • • State transition function
  • • Transaction validation
  • • Block execution

Custom Components

  • • Custom pallets
  • • Runtime APIs
  • • Custom types
  • • Configuration
  • • Extensions

FRAME (Framework for Runtime Aggregation of Modular Entities)

FRAME is the framework used to build Substrate runtimes. It provides a modular approach to runtime development:

FRAME Features:

  • Modular Design: Runtime built from composable pallets
  • Type Safety: Rust's type system ensures safety
  • Hot Swapping: Runtime upgrades without hard forks
  • Standardized APIs: Consistent interfaces across pallets
  • Testing Support: Built-in testing framework

Pallets

Pallets are the building blocks of a Substrate runtime. Each pallet provides specific functionality:

System Pallets

Core pallets that provide essential blockchain functionality.

  • • System: Account management and basic operations
  • • Timestamp: Time management
  • • Balances: Token balance management
  • • Transaction Payment: Fee handling
  • • Sudo: Administrative operations

Consensus Pallets

Pallets that handle consensus and block production.

  • • Aura: Block production
  • • Grandpa: Finality gadget
  • • Babe: Block production
  • • Staking: Validator management
  • • Session: Session management

Utility Pallets

Pallets that provide additional functionality.

  • • Utility: Batch operations
  • • Multisig: Multi-signature support
  • • Proxy: Account proxying
  • • Scheduler: Scheduled operations
  • • Preimage: Preimage management

Runtime Configuration

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 = ();
}

Custom Pallets

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

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)
    }
}

Runtime Upgrades

Substrate supports runtime upgrades without hard forks:

Runtime Upgrade Process:

  • Code Upload: Upload new runtime code to the chain
  • Governance Vote: Vote on the runtime upgrade proposal
  • Execution: Execute the upgrade at a specified block
  • Validation: Validate the new runtime code
  • Activation: Activate the new runtime

Testing Runtimes

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
    });
}

Best Practices

Runtime Development Best Practices:

  • Modular Design: Use pallets for specific functionality
  • Type Safety: Leverage Rust's type system
  • Testing: Write comprehensive tests for all functionality
  • Documentation: Document all public APIs and functions
  • Error Handling: Implement proper error handling
  • Performance: Optimize for runtime performance
  • Security: Follow security best practices
  • Upgrades: Plan for runtime upgrades

Summary

In this chapter, we've explored Substrate runtime development:

  • • Substrate runtime is the core logic of a blockchain
  • • FRAME provides a modular approach to runtime development
  • • Pallets are the building blocks of a runtime
  • • Custom pallets allow for specific functionality
  • • Runtime APIs enable external client interaction
  • • Runtime upgrades allow for evolution without hard forks
  • • Testing is essential for reliable runtime development

In the final chapter, we'll put everything together and learn how to build a complete Substrate-based blockchain from scratch.