Expand description

Learn about Origins, a topic in FRAME that enables complex account abstractions to be built.

§FRAME Origin

Let’s start by clarifying a common wrong assumption about Origin:

ORIGIN IS NOT AN ACCOUNT ID.

FRAME’s origin abstractions allow you to convey meanings far beyond just an account-id being the caller of an extrinsic. Nonetheless, an account-id having signed an extrinsic is one of the meanings that an origin can convey. This is the commonly used frame_system::ensure_signed, where the return value happens to be an account-id.

Instead, let’s establish the following as the correct definition of an origin:

The origin type represents the privilege level of the caller of an extrinsic.

That is, an extrinsic, through checking the origin, can express what privilege level it wishes to impose on the caller of the extrinsic. One of those checks can be as simple as “any account that has signed a statement can pass”.

But the origin system can also express more abstract and complicated privilege levels. For example:

  • If the majority of token holders agreed upon this. This is more or less what the pallet_democracy does under the hood (reference).
  • If a specific ratio of an instance of pallet_collective/DAO agrees upon this.
  • If another consensus system, for example a bridged network or a parachain, agrees upon this.
  • If the majority of validator/authority set agrees upon this1.
  • If caller holds a particular NFT.

and many more.

§Context

First, let’s look at where the origin type is encountered in a typical pallet. The origin: OriginFor<T> has to be the first argument of any given callable extrinsic in FRAME:

#[pallet::call]
impl<T: Config> Pallet<T> {
	pub fn do_something(_origin: OriginFor<T>) -> DispatchResult {
		//              ^^^^^^^^^^^^^^^^^^^^^
		todo!();
	}
}

Typically, the code of an extrinsic starts with an origin check, such as frame_system::ensure_signed.

Note that OriginFor is merely a shorthand for frame_system::Config::RuntimeOrigin. Given the name prefix Runtime, we can learn that RuntimeOrigin is similar to RuntimeCall and others, a runtime composite enum that is amalgamated at the runtime level. Read crate::reference_docs::frame_runtime_types to familiarize yourself with these types.

To understand this better, we will next create a pallet with a custom origin, which will add a new variant to RuntimeOrigin.

§Adding Custom Pallet Origin to the Runtime

For example, given a pallet that defines the following custom origin:

#[pallet::origin]
#[derive(PartialEq, Eq, Clone, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)]
pub enum Origin {
	/// If all holders of a particular NFT have agreed upon this.
	AllNftHolders,
	/// If all validators have agreed upon this.
	ValidatorSet,
}

And a runtime with the following pallets:

construct_runtime!(
	pub struct Runtime {
		System: frame_system,
		PalletWithCustomOrigin: pallet_with_custom_origin,
	}
);

The type crate::reference_docs::frame_origin::runtime_for_origin::RuntimeOrigin is expanded. This RuntimeOrigin contains a variant for the frame_system::RawOrigin and the custom origin of the pallet.

Notice how the frame_system::ensure_signed is nothing more than a match statement. If you want to know where the actual origin of an extrinsic is set (and the signature verification happens, if any), see sp_runtime::generic::CheckedExtrinsic, specifically sp_runtime::traits::Applyable’s implementation.

§Asserting on a Custom Internal Origin

In order to assert on a custom origin that is defined within your pallet, we need a way to first convert the <T as frame_system::Config>::RuntimeOrigin into the local enum Origin of the current pallet. This is a common process that is explained in crate::reference_docs::frame_runtime_types.

We use the same process here to express that RuntimeOrigin has a number of additional bounds, as follows.

  1. Defining a custom RuntimeOrigin with further bounds in the pallet.
#[pallet::config]
pub trait Config: frame_system::Config {
	type RuntimeOrigin: From<<Self as frame_system::Config>::RuntimeOrigin>
		+ Into<Result<Origin, <Self as Config>::RuntimeOrigin>>;
}
  1. Using it in the pallet.
#[pallet::call]
impl<T: Config> Pallet<T> {
	pub fn only_validators(origin: OriginFor<T>) -> DispatchResult {
		// first, we convert from `<T as frame_system::Config>::RuntimeOrigin` to `<T as
		// Config>::RuntimeOrigin`
		let local_runtime_origin = <<T as Config>::RuntimeOrigin as From<
			<T as frame_system::Config>::RuntimeOrigin,
		>>::from(origin);
		// then we convert to `origin`, if possible
		let local_origin =
			local_runtime_origin.into().map_err(|_| "invalid origin type provided")?;
		ensure!(matches!(local_origin, Origin::ValidatorSet), "Not authorized");
		todo!();
	}
}

§Asserting on a Custom External Origin

Very often, a pallet wants to have a parameterized origin that is NOT defined within the pallet. In other words, a pallet wants to delegate an origin check to something that is specified later at the runtime level. Like many other parameterizations in FRAME, this implies adding a new associated type to trait Config.

#[pallet::config]
pub trait Config: frame_system::Config {
	type ExternalOrigin: EnsureOrigin<Self::RuntimeOrigin>;
}

Then, within the pallet, we can simply use this “unknown” origin check type:

#[pallet::call]
impl<T: Config> Pallet<T> {
	pub fn externally_checked_ext(origin: OriginFor<T>) -> DispatchResult {
		let _ = T::ExternalOrigin::ensure_origin(origin)?;
		todo!();
	}
}

Finally, at the runtime, any implementation of frame::traits::EnsureOrigin can be passed.

impl pallet_with_external_origin::Config for Runtime {
	type ExternalOrigin = EnsureSigned<<Self as frame_system::Config>::AccountId>;
}

Indeed, some of these implementations of frame::traits::EnsureOrigin are similar to the ones that we know about: frame::runtime::prelude::EnsureSigned, frame::runtime::prelude::EnsureSignedBy, frame::runtime::prelude::EnsureRoot, frame::runtime::prelude::EnsureNone, etc. But, there are also many more that are not known to us, and are defined in other pallets.

For example, pallet_collective defines pallet_collective::EnsureMember and pallet_collective::EnsureProportionMoreThan and many more, which is exactly what we alluded to earlier in this document.

Make sure to check the full list of implementors of EnsureOrigin for more inspiration.

§Obtaining Abstract Origins

So far we have learned that FRAME pallets can assert on custom and abstract origin types, whether they are defined within the pallet or not. But how can we obtain these abstract origins?

All extrinsics that come from the outer world can generally only be obtained as either signed or none origin.

Generally, these abstract origins are only obtained within the runtime, when a call is dispatched within the runtime.

§Further References


  1. Inherents are essentially unsigned extrinsics that need an frame_system::ensure_none origin check, and through the virtue of being an inherent, are agreed upon by all validators. 

Modules§