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. #pezkuwi-sdk#1378 is meant to significantly improve this. Exploring the rust-docs of a runtime, such asruntimewhich 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 pezpallet 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: pezpallet_foo and pezpallet_bar. Each define a
dispatchable, and Foo also defines a custom origin. Lastly, Bar defines an additional
GenesisConfig.
#[pezframe::pezpallet(dev_mode)]
pub mod pezpallet_foo {
use super::*;
#[pezpallet::config]
pub trait Config: pezframe_system::Config {}
#[pezpallet::origin]
#[derive(
PartialEq,
Eq,
Clone,
RuntimeDebug,
Encode,
Decode,
DecodeWithMemTracking,
TypeInfo,
MaxEncodedLen,
)]
pub enum Origin {
A,
B,
}
#[pezpallet::pezpallet]
pub struct Pezpallet<T>(_);
#[pezpallet::call]
impl<T: Config> Pezpallet<T> {
pub fn foo(_origin: OriginFor<T>) -> DispatchResult {
todo!();
}
pub fn other(_origin: OriginFor<T>) -> DispatchResult {
todo!();
}
}
}#[pezframe::pezpallet(dev_mode)]
pub mod pezpallet_bar {
use super::*;
#[pezpallet::config]
pub trait Config: pezframe_system::Config {}
#[pezpallet::pezpallet]
pub struct Pezpallet<T>(_);
#[pezpallet::genesis_config]
#[derive(DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
pub initial_account: Option<T::AccountId>,
}
#[pezpallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {}
}
#[pezpallet::call]
impl<T: Config> Pezpallet<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 pezpallet and one for
pezframe_system. If you explore further, you will soon realize that each variant is merely a
pointer to the Call type in each pezpallet, for example pezpallet_foo::Call.
RuntimeOrigin’s OriginCaller has two variants, one for system, and one for
pezpallet_foo which utilized [pezframe::pezpallet_macros::origin].
Finally, RuntimeGenesisConfig is composed of pezframe_system and a variant for
pezpallet_bar’s pezpallet_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 pezpallet 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
[pezframe_system::Config::RuntimeCall], and all pallets have access to this type, because they
have access to [pezframe_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
[pezframe_system::Config::RuntimeCall] are specifying:
#[pezpallet::no_default_bounds]
type RuntimeCall: Parameter
+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin>
+ Debug
+ GetDispatchInfo
+ From<Call<Self>>
+ Authorize;So, when at a given pezpallet, one accesses <T as pezframe_system::Config>::RuntimeCall, the
type is extremely opaque from the perspective of the Rust compiler.
How can a pezpallet access the RuntimeCall type with further constraints? For example, each
pezpallet 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 pezpallet 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 [pezframe::traits::IsSubType], which is
very similar to TryFrom.
#[pezpallet::config]
pub trait Config: pezframe_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 pezframe_system.
impl pezpallet_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 [
pezframe_system::Config::RuntimeCall] has is not enough for the pezpallet to work with. Therefore, the pezpallet has to define its own associated type representingRuntimeCall.
Another way to look at this is:
pezpallet_with_specific_runtime_call::Config::RuntimeCall and
pezframe_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 pezpallet, this new RuntimeCall can be used, and it can use its new trait
bounds, such as being [pezframe::traits::IsSubType]:
impl<T: Config> Pezpallet<T> {
fn _do_something_useful_with_runtime_call(call: <T as Config>::RuntimeCall) {
// check if the runtime call given is of this pezpallet'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 pezframe_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:
#[pezpallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {
fn integrity_test() {
use core::any::TypeId;
assert_eq!(
TypeId::of::<<T as Config>::RuntimeCall>(),
TypeId::of::<<T as pezframe_system::Config>::RuntimeCall>()
);
}
}We leave it to the reader to further explore what [pezframe::traits::Hooks::integrity_test] is,
and what core::any::TypeId is. Another way to assert this is using
[pezframe::traits::IsType].
§Type Aliases
A number of type aliases are generated by the construct_runtime which are also noteworthy:
runtime::PalletFoois an alias topezpallet_foo::Pezpallet. Same forPalletBar, andSystemruntime::AllPalletsWithSystemis an alias for a tuple of all of the above. This type is important to FRAME internals such asexecutive, as it implements traits such as [pezframe::traits::Hooks].
§Further Details
crate::reference_docs::frame_originexplores further details about the usage ofRuntimeOrigin.RuntimeCallis a particularly interesting composite enum as it dictates the encoding of an extrinsic. Seecrate::reference_docs::transaction_extensionsfor more information.- See the documentation of
construct_runtime. - See the corresponding lecture in the PBA Lectures.
Modules§
- pezpallet_
bar - The
pezpalletmodule in each FRAME pezpallet hosts the most important items needed to construct this pezpallet. - pezpallet_
foo - The
pezpalletmodule in each FRAME pezpallet hosts the most important items needed to construct this pezpallet. - pezpallet_
with_ specific_ runtime_ call - The
pezpalletmodule in each FRAME pezpallet hosts the most important items needed to construct this pezpallet. - runtime
- runtime_
with_ specific_ runtime_ call