Runtime development

As discussed in Architecture, the runtime for a Substrate node contains all of the business logic for executing transactions, saving state transitions, and interacting with the outer node. Substrate provides all of the tools required to build common blockchain components so you can focus on developing the runtime logic that defines the blockchain behavior.

State transitions and the runtime

At the most basic level, every blockchain is essentially a ledger or record of each change that takes place on-chain. In Substrate-based chains, these changes to state are recorded in the runtime. Because the runtime handles this operation, the runtime is sometimes described as providing the state transition function.

Because state transitions occur in the runtime, the runtime is where you define the storage items that represent the blockchain state and the transactions that allow blockchain users to make changes to this state.

State and functions in the runtime

The Substrate runtime determines which transactions are valid and invalid and how the chain state is changed in response to transactions.

Runtime interfaces

As you learned in Architecture, the outer node is responsible for handling peer discovery, transaction pooling, block and transaction gossiping, consensus, and answering RPC calls from the outside world. These tasks frequently require the outer node to query the runtime for information or to provide information to the runtime. The runtime API facilitates this kind of communication between the outer node and the runtime.

In Substrate, the sp_api crate provides an interface to implement a runtime API. It is designed to give you flexibility in defining your own custom interfaces using the impl_runtime_apis macro. However, every runtime must implement the Core and Metadata interfaces. In addition to these required interfaces, most Substrate nodes—like the node template—implement the following runtime interfaces:

Core primitives

Substrate also defines the core primitives that the runtime must implement. The Substrate framework makes minimal assumptions about what your runtime must provide to the other layers of Substrate. However, there are a few data types that must be defined and must fulfill a particular interface to work within the Substrate framework.

These core primitives are:

  • Hash: A type which encodes a cryptographic digest of some data. Typically just a 256-bit quantity.
  • DigestItem: A type which must be able to encode one of a number of "hard-wired" alternatives relevant to consensus and change-tracking as well as any number of "soft-coded" variants, relevant to specific modules within the runtime.
  • Digest: A series of DigestItems. This encodes all information that is relevant for a light-client to have on hand within the block.
  • Extrinsic: A type to represent a single piece of data external to the blockchain that is recognized by the blockchain. This typically involves one or more signatures, and some sort of encoded instructions (e.g. for transferring ownership of funds or calling into a smart contract).
  • Header: A type which is representative (cryptographically or otherwise) of all information relevant to a block. It includes the parent hash, the storage root and the extrinsics trie root, the digest and a block number.
  • Block: Essentially just a combination of Header and a series of Extrinsics, together with a specification of the hashing algorithm to be used.
  • BlockNumber: A type which encodes the total number of ancestors any valid block has. Typically a 32-bit quantity.

FRAME

FRAME is one of the most powerful tools available to you as a runtime developer. As mentioned in Substrate empowers developers, FRAME is an acronym for Framework for Runtime Aggregation of Modularized Entities and it encompasses a significant number of modules and support libraries that simplify runtime development. In Substrate, these modules—called pallets—offer customizable business logic for different use cases and features that you might want to include in your runtime. For example, there are pallets that provide a framework of business logic for staking, consensus, governance, and other common activities.

For a summary of the pallets available, see FRAME pallets.

In addition to pallets, FRAME provides services to interact with the runtime through the following libraries and modules

The following diagram illustrates how FRAME and its system, support, and executives modules provide services for the runtime environment.

FRAME and the runtime architecture

Composing a runtime with pallets

You can build a Substrate-based blockchain without using FRAME. However, FRAME pallets enable you to compose custom runtime logic using predefined components as a starting point. Each pallet defines specific types, storage items, and functions to implement a specific set of features or functionality for a runtime.

The following diagram illustrates how you can select and combine FRAME pallets to compose a runtime.

Compose a runtime with FRAME

Building custom pallets

In addition to the library of pre-built FRAME pallets, you can use the FRAME libraries and services to build your own custom pallets. With custom pallets, you have the flexibility to define the runtime behavior that best suits your purposes. Because each pallet has its own discrete logic, you can combine pre-built and custom pallets to control the features and functionality your blockchain provides and achieve the results you want.

For example, you might include the Balances pallet in your runtime to use its cryptocurrency-related storage items and functions for managing tokens, but add custom logic to call a pallet you write when an account balance changes.

Most pallets are composed with some combination of the following sections:

  • Imports and dependencies
  • Pallet type declaration
  • Runtime configuration trait
  • Runtime storage
  • Runtime events
  • Hooks for logic that should be executed in a specific context
  • Function calls that can be used to execute transactions

For example, if you wanted to define a custom pallet, you might start with a skeleton structure for the pallet similar to the following:

// Add required imports and dependencies
pub use pallet::*;

#[frame_support::pallet]
pub mod pallet {
 use frame_support::pallet_prelude::*;
 use frame_system::pallet_prelude::*;

 // Declare the pallet type
 // This is a placeholder to implement traits and methods.
 #[pallet::pallet]
 pub struct Pallet<T>(_);

 // Add the runtime configuration trait
 // All types and constants go here.
 #[pallet::config]
 pub trait Config: frame_system::Config { ... }

 // Add runtime storage to declare storage items.
 #[pallet::storage]
 #[pallet::getter(fn something)]
 pub type MyStorage<T: Config> = StorageValue<_, u32>;

 // Add runtime events
 #[pallet::event]
 #[pallet::generate_deposit(pub(super) fn deposit_event)]
 pub enum Event<T: Config> { ... }

 // Add hooks to define some logic that should be executed
 // in a specific context, for example on_initialize.
 #[pallet::hooks]
 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { ... }

 // Add functions that are callable from outside the runtime.
 #[pallet::call]
 impl<T:Config> Pallet<T> { ... }
}

You can compose pallets with some or all of the sections, as needed. As you start to design and build your custom runtime, you'll learn more about FRAME libraries and the runtime primitives used to define configuration traits, storage items, events, and errors, and how to write the function calls that are dispatched to the runtime for execution.

Where to go next

Now that you are familiar with the basics of Substrate runtime development and working with pallets, explore the following topics and tutorials to learn more.