Skip to main content

Deconstructing substrate pallet genesis config

Substrate provides a smooth interface that allows blockchain runtime developers to define the configuration of pallet storage at the genesis block of the runtime or a pallet instantiation.

Substrate uses genesis_config and genesis_build macros in combination with the GenesisBuild trait to abstract complex APIs and processes involved in the initialization of a pallet's storage.

In this guide, we will dive deep into the internals of substrate pallet storage configuration using a trivial example implemented on a double auction pallet.

Help us measure our progress and improve Substrate in Bits content by filling out our living feedback form. Thank you!

Reproducing setup

Environment and project setup

To follow along with this tutorial, ensure that you have the Rust toolchain installed.

git clone https://github.com/cenwadike/double-auction-pallet
  • Navigate into the project’s directory.
cd double-auction
  • Run the command below to build the pallet.
cargo build --release

Getting some context

The setup above is a substrate pallet that implements double-auction for electrical energy.

The seller gets matched (and their electricity is sold to the highest bidder) when the auction period is over.

Auctions to be executed are stored in AuctionsExecutionQueue. When an auction period is over, it is taken from the AuctionsExecutionQueue and matched to the highest bidder.

Using substrate genesis_config we initialize AuctionIndex.

AuctionIndex is an incremental counter that assigns a unique id to an Auction.

Adding pallet genesis config

To add the genesis configuration of a pallet storage item, we need to follow five steps:

  • Define the storage items.
  • Add storage item to GenesisConfig.
  • Define default values of storage items.
  • Implement GenesisBuild for GenesisConfig.
  • Define concrete values for storage items.

NB: The code used in this guide prioritizes readability over conciseness. Feel free to use more concise code where you see fit.

Defining storage item

We used substrate StorageValue to define AuctionIndex as an item like so:

  #[pallet::storage]
#[pallet::getter(fn auctions_index)]
pub(super) type AuctionIndex<T: Config> = StorageValue<_, u64>;

Adding the storage item to GenesisConfig

Substrate defines a convention to simplify the definition of a pallet genesis configuration to ease integration with other parts of substrate including substrate runtime and outer node.

We can define the genesis configuration as a struct like so:

  // define pallet's genesis configuration
#[pallet::genesis_config]
pub struct GenesisConfig {
pub auction_index: u64,
}

We could also define genesis config as an enum, however for this example, we will stick to using a struct.

Defining the default value for a storage item

Defining a default value allows us to defer hard-coding initial values on a storage items. The assignment of the initial value is deferred to the runtime that implements our pallet genesis configuration.

The default value for AuctionIndex is defined like so:

  // assign default value for storage items
#[cfg(feature = "std")]
impl Default for GenesisConfig {
fn default() -> Self {
Self {
auction_index: Default::default(),
}
}
}

This uses Rust's in-built default value for u64.

We could also define custom default values for complex types including substrate generic types. A link to a substrate doc describing how to use generic types will is added at the end of this guide.

Implementing GenesisBuild for GenesisConfig

We can now implement the GenesisBuild trait on our GenesisConfig to expose a build function that is leveraged by the runtime to initialize the pallet storage items.

The code below implements GenesisBuild for GenesisConfig and defines the build function where the initial storage is passed into AuctionIndex.

  #[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig {
fn build(&self) {
// custom values are added here at the genesis block
<AuctionIndex<T>>::put(&self.auction_index);
}
}

Setting initial values for storage items

This step is vital to ensure that the genesis configuration works as expected in the mock runtime and as well as the main runtime.

After coupling your runtime/lib.rs like so:

// Configure a mock runtime to test the pallet.
frame_support::construct_runtime!(
pub struct Runtime
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system,
DoubleAuctionModule: pallet_double_auction,

// -------------snip-----------
}
);

Add an initial value for AuctionIndex in node/chain_spec.rs like so:

const AUCTION_INDEX: u64 = 10001;

You can then use this AUCTION_INDEX in node/chain_spec.rs like so:

GenesisConfig {
// -------------snip-----------

pallet_double_auction: DoubleAuctionModule {
auction_index: AUCTION_INDEX,
},

// -------------snip-----------
}

The runtime uses the data provided in node/chain_spec.rs to initialize AuctionIndex at the genesis block.

Going in-depth

At a glance, Substrate storage genesis configuration may appear to be an unnecessary endeavor that could be easily satisfied by declaring the initial storage item directly in the pallet. However, the moment you attempt to do this, you will realize that it is not an easy feat. This becomes especially true when you attempt to initialize a storage item with generic types.

Substrate provides a developer-friendly interface that enables us to design and implement runtime modules that can be used in different runtimes without the need to rewrite the module's configuration for different runtimes.

This interface is provided mainly by two attribute macros; genesis_config and genesis_build, and a trait GenesisBuild which must be used together to provide a cohesive abstraction for complex storage handling and runtime operations.

GenesisConfig decorated with genesis_config allows us to define a data type as a struct or enum for the configuration of our pallet's genesis state. genesis_config also enforces that GenesisConfig can only be used from a pallet.

GenesisConfig also implements low-level methods that can be leveraged by the substrate runtime to add values from GenesisConfig to storage (even after the genesis block).

GenesisConfig is defined like so:

pub struct GenesisConfig {
pub changes_trie_config: Option<ChangesTrieConfiguration>,
pub code: Vec<u8>,
}

And has an implementation block like so:

impl GenesisConfig {
/// Direct implementation of `GenesisBuild::build_storage`.
///
/// Kept in order not to break dependency.
pub fn build_storage<T: Config>(&self) -> Result<sp_runtime::Storage, String> {
<Self as GenesisBuild<T>>::build_storage(self)
}

/// Direct implementation of `GenesisBuild::assimilate_storage`.
///
/// Kept in order not to break dependency.
pub fn assimilate_storage<T: Config>(
&self,
storage: &mut sp_runtime::Storage,
) -> Result<(), String> {
<Self as GenesisBuild<T>>::assimilate_storage(self, storage)
}
}

The methods in the impl block are no longer the reference standard for implementing genesis configuration using substrate FRAME and as such require the GenesisBuild trait.

The GenesisBuild trait along with genesis_build macro allows us to define exactly how the genesis configuration is built for each storage item provided.

GenesisBuild is defined like so:

pub trait GenesisBuild<T, I = ()>: Default + MaybeSerializeDeserialize {
/// The build function is called within an externalities allowing storage APIs.
/// Thus one can write to storage using regular pallet storages.
fn build(&self);

/// Build the storage using `build` inside default storage.
fn build_storage(&self) -> Result<sp_runtime::Storage, String> {
let mut storage = Default::default();
self.assimilate_storage(&mut storage)?;
Ok(storage)
}

/// Assimilate the storage for this module into pre-existing overlays.
fn assimilate_storage(&self, storage: &mut sp_runtime::Storage) -> Result<(), String> {
sp_state_machine::BasicExternalities::execute_with_storage(storage, || {
self.build();
Ok(())
})
}
}

Both build_storage and assimilate_storage work exactly as in GenesisConfig impl.

build is the method of interest. Substrate uses the build method to directly expose storage API to a pallet at the genesis block.

In our case, substrate stores the initial storage value of AuctionIndex under the key provided by the storage instance. This allows a pallet with multiple instances to also have multiple variants of AuctionIndex.

Summary

We used the AuctionIndex of a double auction pallet to demonstrate how to implement a genesis configuration using Substrate FRAME pallets and APIs. We explored the steps involved in implementing a genesis config, which allows our pallet to be used on any substrate runtime with arbitrary initial value.

We developed an understanding of:

  • why genesis configurations are important and useful.
  • substrate abstraction for genesis configuration.
  • how to implement genesis configuration on a pallet.
  • how to add initial genesis configuration values in node chain-spec.

To learn more about substrate genesis config, check out these resources:

Help us measure our progress and improve Substrate in Bits content by filling out our living feedback form. Thank you!

grillchat icon