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 as runtime 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 representing RuntimeCall.

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:

§Further Details

Modules§