referrerpolicy=no-referrer-when-downgrade

Crate pallet_parameters

source ·
Expand description

§⚠️ WARNING ⚠️


THIS CRATE IS NOT AUDITED AND SHOULD NOT BE USED IN PRODUCTION.

§Parameters

Allows to update configuration parameters at runtime.

§Pallet API

This pallet exposes two APIs; one inbound side to update parameters, and one outbound side to access said parameters. Parameters themselves are defined in the runtime config and will be aggregated into an enum. Each parameter is addressed by a key and can have a default value. This is not done by the pallet but through the frame_support::dynamic_params::dynamic_params macro or alternatives.

Note that this is incurring one storage read per access. This should not be a problem in most cases but must be considered in weight-restrained code.

§Inbound

The inbound side solely consists of the Pallet::set_parameter extrinsic to update the value of a parameter. Each parameter can have their own admin origin as given by the Config::AdminOrigin.

§Outbound

The outbound side is runtime facing for the most part. More general, it provides a Get implementation and can be used in every spot where that is accepted. Two macros are in place: [frame_support::dynamic_params::define_parameters and [frame_support::dynamic_params:dynamic_pallet_params] to define and expose parameters in a typed manner.

See the pallet module for more information about the interfaces this pallet exposes, including its configuration trait, dispatchables, storage items, events and errors.

§Overview

This pallet is a good fit for updating parameters without a runtime upgrade. It is very handy to not require a runtime upgrade for a simple parameter change since runtime upgrades require a lot of diligence and always bear risks. It seems overkill to update the whole runtime for a simple parameter change. This pallet allows for fine-grained control over who can update what. The only down-side is that it trades off performance with convenience and should therefore only be used in places where that is proven to be uncritical. Values that are rarely accessed but change often would be a perfect fit.

§Example Configuration

Here is an example of how to define some parameters, including their default values:

#[dynamic_params(RuntimeParameters, pallet_parameters::Parameters::<Runtime>)]
pub mod dynamic_params {
	use super::*;

	#[dynamic_pallet_params]
	#[codec(index = 3)]
	pub mod pallet1 {
		#[codec(index = 0)]
		pub static Key1: u64 = 0;
		#[codec(index = 1)]
		pub static Key2: u32 = 1;
		#[codec(index = 2)]
		pub static Key3: u128 = 2;
	}

	#[dynamic_pallet_params]
	#[codec(index = 1)]
	pub mod pallet2 {
		#[codec(index = 2)]
		pub static Key1: u64 = 0;
		#[codec(index = 1)]
		pub static Key2: u32 = 2;
		#[codec(index = 0)]
		pub static Key3: u128 = 4;
	}

	#[dynamic_pallet_params]
	#[codec(index = 2)]
	pub mod nis {
		#[codec(index = 0)]
		pub static Target: u64 = 0;
	}

	#[dynamic_pallet_params]
	#[codec(index = 3)]
	pub mod somE_weird_SPElLInG_s {
		#[codec(index = 0)]
		pub static V: u64 = 0;
	}
}

A permissioned origin can be define on a per-key basis like this:

mod custom_origin {
	use super::*;
	pub struct ParamsManager;

	impl EnsureOriginWithArg<RuntimeOrigin, RuntimeParametersKey> for ParamsManager {
		type Success = ();

		fn try_origin(
			origin: RuntimeOrigin,
			key: &RuntimeParametersKey,
		) -> Result<Self::Success, RuntimeOrigin> {
			// Account 123 is allowed to set parameters in benchmarking only:
			#[cfg(feature = "runtime-benchmarks")]
			if ensure_signed(origin.clone()).map_or(false, |acc| acc == 123) {
				return Ok(());
			}

			match key {
				RuntimeParametersKey::SomEWeirdSPElLInGS(_) |
				RuntimeParametersKey::Nis(_) |
				RuntimeParametersKey::Pallet1(_) => ensure_root(origin.clone()),
				RuntimeParametersKey::Pallet2(_) => ensure_signed(origin.clone()).map(|_| ()),
			}
			.map_err(|_| origin)
		}

		#[cfg(feature = "runtime-benchmarks")]
		fn try_successful_origin(_key: &RuntimeParametersKey) -> Result<RuntimeOrigin, ()> {
			Ok(RuntimeOrigin::signed(123))
		}
	}
}

The pallet will also require a default value for benchmarking. Ideally this is the variant with the longest encoded length. Although in either case the PoV benchmarking will take the worst case over the whole enum.

#[cfg(feature = "runtime-benchmarks")]
impl Default for RuntimeParameters {
	fn default() -> Self {
		RuntimeParameters::Pallet1(dynamic_params::pallet1::Parameters::Key1(
			dynamic_params::pallet1::Key1,
			Some(123),
		))
	}
}

Now the aggregated parameter needs to be injected into the pallet config:

#[derive_impl(pallet_parameters::config_preludes::TestDefaultConfig)]
impl Config for Runtime {
	type AdminOrigin = custom_origin::ParamsManager;
	// RuntimeParameters is injected by the `derive_impl` macro.
	// RuntimeEvent is injected by the `derive_impl` macro.
	// WeightInfo is injected by the `derive_impl` macro.
}

As last step, the parameters can now be used in other pallets 🙌

impl pallet_example_basic::Config for Runtime {
	// Use the dynamic key in the pallet config:
	type MagicNumber = dynamic_params::pallet1::Key1;

	type RuntimeEvent = RuntimeEvent;
	type WeightInfo = ();
}

§Examples Usage

Now to demonstrate how the values can be updated:

#[test]
fn set_parameters_example() {
	new_test_ext().execute_with(|| {
		assert_eq!(pallet1::Key3::get(), 2, "Default works");

		// This gets rejected since the origin is not root.
		assert_noop!(
			PalletParameters::set_parameter(
				Origin::signed(1),
				Pallet1(pallet1::Parameters::Key3(pallet1::Key3, Some(123))),
			),
			DispatchError::BadOrigin
		);

		assert_ok!(PalletParameters::set_parameter(
			Origin::root(),
			Pallet1(pallet1::Parameters::Key3(pallet1::Key3, Some(123))),
		));

		assert_eq!(pallet1::Key3::get(), 123, "Update works");
		assert_last_event(
			crate::Event::Updated {
				key: RuntimeParametersKey::Pallet1(pallet1::ParametersKey::Key3(pallet1::Key3)),
				old_value: None,
				new_value: Some(RuntimeParametersValue::Pallet1(pallet1::ParametersValue::Key3(
					123,
				))),
			}
			.into(),
		);
	});
}

§Low Level / Implementation Details

The pallet stores the parameters in a storage map and implements the matching Get<Value> for each Key type. The Get then accesses the Parameters map to retrieve the value. An event is emitted every time that a value was updated. It is even emitted when the value is changed to the same.

The key and value types themselves are defined by macros and aggregated into a runtime wide enum. This enum is then injected into the pallet. This allows it to be used without any changes to the pallet that the parameter will be utilized by.

§Design Goals

  1. Easy to update without runtime upgrade.
  2. Exposes metadata and docs for user convenience.
  3. Can be permissioned on a per-key base.

§Design

  1. Everything is done at runtime without the need for const values. Get allows for this - which is coincidentally an upside and a downside. 2. The types are defined through macros, which allows to expose metadata and docs. 3. Access control is done through the EnsureOriginWithArg trait, that allows to pass data along to the origin check. It gets passed in the key. The implementor can then match on the key and the origin to decide whether the origin is permissioned to set the value.

Re-exports§

Modules§

  • The pallet module in each FRAME pallet hosts the most important items needed to construct this pallet.

Traits§

  • Weight functions needed for pallet_parameters.