Expand description
Learn about composite enums and other runtime level types, such as RuntimeEvent
and
RuntimeCall
.
§FRAME Runtime Types
This reference document briefly explores the idea around types generated at the runtime level by the FRAME macros.
As of now, many of these important types are generated within the internals of
construct_runtime
, and there is no easy way for you to visually know they exist. #polkadot-sdk#1378 is meant to significantly improve this. Exploring the rust-docs of a runtime, such asruntime
which is defined in this module is as of now the best way to learn about these types.
§Composite Enums
Many types within a FRAME runtime follow the following structure:
- Each individual pallet defines a type, for example
Foo
. - At the runtime level, these types are amalgamated into a single type, for example
RuntimeFoo
.
As the names suggest, all composite enums in a FRAME runtime start their name with Runtime
.
For example, RuntimeCall
is a representation of the most high level Call
-able type in the
runtime.
Composite enums are generally convertible to their individual parts as such:
flowchart LR RuntimeCall --"TryInto"--> PalletCall PalletCall --"Into"--> RuntimeCall
In that one can always convert from the inner type into the outer type, but not vice versa. This
is usually expressed by implementing From
, TryFrom
, From<Result<_>>
and similar traits.
§Example
We provide the following two pallets: pallet_foo
and pallet_bar
. Each define a
dispatchable, and Foo
also defines a custom origin. Lastly, Bar
defines an additional
GenesisConfig
.
#[frame::pallet(dev_mode)]
pub mod pallet_foo {
use super::*;
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::origin]
#[derive(PartialEq, Eq, Clone, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)]
pub enum Origin {
A,
B,
}
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::call]
impl<T: Config> Pallet<T> {
pub fn foo(_origin: OriginFor<T>) -> DispatchResult {
todo!();
}
pub fn other(_origin: OriginFor<T>) -> DispatchResult {
todo!();
}
}
}
#[frame::pallet(dev_mode)]
pub mod pallet_bar {
use super::*;
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::genesis_config]
#[derive(DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
pub initial_account: Option<T::AccountId>,
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
pub fn bar(_origin: OriginFor<T>) -> DispatchResult {
todo!();
}
}
}
Let’s explore how each of these affect the RuntimeCall
, RuntimeOrigin
and
RuntimeGenesisConfig
generated in runtime
respectively.
As observed, RuntimeCall
has 3 variants, one for each pallet and one for frame_system
. If
you explore further, you will soon realize that each variant is merely a pointer to the Call
type in each pallet, for example pallet_foo::Call
.
RuntimeOrigin
’s OriginCaller
has two variants, one for system, and one for pallet_foo
which utilized frame::pallet_macros::origin
.
Finally, RuntimeGenesisConfig
is composed of frame_system
and a variant for pallet_bar
’s
pallet_bar::GenesisConfig
.
You can find other composite enums by scanning runtime
for other types who’s name starts
with Runtime
. Some of the more noteworthy ones are:
§Adding Further Constraints to Runtime Composite Enums
This section explores a common scenario where a pallet has access to one of these runtime composite enums, but it wishes to further specify it by adding more trait bounds to it.
Let’s take the example of RuntimeCall
. This is an associated type in
frame_system::Config::RuntimeCall
, and all pallets have access to this type, because they
have access to frame_system::Config
. Finally, this type is meant to be set to outer call of
the entire runtime.
But, let’s not forget that this is information that we know, and the Rust compiler does not.
All that the rust compiler knows about this type is ONLY what the trait bounds of
frame_system::Config::RuntimeCall
are specifying:
#[pallet::no_default_bounds]
type RuntimeCall: Parameter
+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin>
+ Debug
+ From<Call<Self>>;
So, when at a given pallet, one accesses <T as frame_system::Config>::RuntimeCall
, the type is
extremely opaque from the perspective of the Rust compiler.
How can a pallet access the RuntimeCall
type with further constraints? For example, each
pallet has its own enum Call
, and knows that its local Call
is a part of RuntimeCall
,
therefore there should be a impl From<Call<_>> for RuntimeCall
.
The only way to express this using Rust’s associated types is for the pallet to define its own
associated type RuntimeCall
, and further specify what it thinks RuntimeCall
should be.
In this case, we will want to assert the existence of frame::traits::IsSubType
, which is
very similar to TryFrom
.
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeCall: IsSubType<Call<Self>>;
}
And indeed, at the runtime level, this associated type would be the same RuntimeCall
that is
passed to frame_system
.
impl pallet_with_specific_runtime_call::Config for Runtime {
// an implementation of `IsSubType` is provided by `construct_runtime`.
type RuntimeCall = RuntimeCall;
}
In other words, the degree of specificity that
frame_system::Config::RuntimeCall
has is not enough for the pallet to work with. Therefore, the pallet has to define its own associated type representingRuntimeCall
.
Another way to look at this is:
pallet_with_specific_runtime_call::Config::RuntimeCall
and frame_system::Config::RuntimeCall
are two different representations of the same concrete type that is only known when the runtime
is being constructed.
Now, within this pallet, this new RuntimeCall
can be used, and it can use its new trait
bounds, such as being frame::traits::IsSubType
:
impl<T: Config> Pallet<T> {
fn _do_something_useful_with_runtime_call(call: <T as Config>::RuntimeCall) {
// check if the runtime call given is of this pallet's variant.
let _maybe_my_call: Option<&Call<T>> = call.is_sub_type();
todo!();
}
}
Once Rust’s “Associated Type Bounds RFC” is usable, this syntax can be used to simplify the above scenario. See this issue for more information.
§Asserting Equality of Multiple Runtime Composite Enums
Recall that in the above example, <T as Config>::RuntimeCall
and <T as frame_system::Config>::RuntimeCall
are expected to be equal types, but at the compile-time we
have to represent them with two different associated types with different bounds. Would it not
be cool if we had a test to make sure they actually resolve to the same concrete type once the
runtime is constructed? The following snippet exactly does that:
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn integrity_test() {
use core::any::TypeId;
assert_eq!(
TypeId::of::<<T as Config>::RuntimeCall>(),
TypeId::of::<<T as frame_system::Config>::RuntimeCall>()
);
}
}
We leave it to the reader to further explore what frame::traits::Hooks::integrity_test
is,
and what core::any::TypeId
is. Another way to assert this is using
frame::traits::IsType
.
§Type Aliases
A number of type aliases are generated by the construct_runtime
which are also noteworthy:
runtime::PalletFoo
is an alias topallet_foo::Pallet
. Same forPalletBar
, andSystem
runtime::AllPalletsWithSystem
is an alias for a tuple of all of the above. This type is important to FRAME internals such asexecutive
, as it implements traits such asframe::traits::Hooks
.
§Further Details
crate::reference_docs::frame_origin
explores further details about the usage ofRuntimeOrigin
.RuntimeCall
is a particularly interesting composite enum as it dictates the encoding of an extrinsic. Seecrate::reference_docs::signed_extensions
for more information.- See the documentation of
construct_runtime
. - See the corresponding lecture in the pba-book.
Modules§
- The
pallet
module in each FRAME pallet hosts the most important items needed to construct this pallet. - The
pallet
module in each FRAME pallet hosts the most important items needed to construct this pallet. - The
pallet
module in each FRAME pallet hosts the most important items needed to construct this pallet.