referrerpolicy=no-referrer-when-downgrade
Expand description

Learn about chain specification file and the genesis state of the blockchain.

§What is a chain specification

A chain specification file defines the set of properties that are required to run the node as part of the chain. The chain specification consists of two main parts:

  • initial state of the runtime,
  • network / logical properties of the chain, the most important property being the list of bootnodes.

This document describes how the initial state is handled in pallets and runtime, and how to interact with the runtime in order to build the genesis state.

For more information on chain specification and its properties, refer to sc_chain_spec.

The initial genesis state can be provided in the following formats:

  • full
  • patch
  • raw

Each of the formats is explained in chain-spec-format.

§GenesisConfig for pallet

Every frame pallet may have its initial state which is defined by the GenesisConfig internal struct. It is a regular Rust struct, annotated with the pallet::genesis_config attribute.

#[pallet::genesis_config]
#[derive(DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
	pub initial_account: Option<T::AccountId>,
}

The struct shall be defined within the pallet mod, as in the following code:

#[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::storage]
	pub(super) type InitialAccount<T: Config> = StorageValue<Value = T::AccountId>;

	/// Simple `GenesisConfig`.
	#[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> {
		/// The storage building function that presents a direct mapping of the initial config
		/// values to the storage items.
		fn build(&self) {
			InitialAccount::<T>::set(self.initial_account.clone());
		}
	}
}

The initial state conveyed in the GenesisConfig struct is transformed into state storage items by means of the BuildGenesisConfig trait, which shall be implemented for the pallet’s GenesisConfig struct. The pallet::genesis_build attribute shall be attached to the impl block:

#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
	/// The storage building function that presents a direct mapping of the initial config
	/// values to the storage items.
	fn build(&self) {
		InitialAccount::<T>::set(self.initial_account.clone());
	}
}

GenesisConfig may also contain more complicated types, including nested structs or enums, as in the example for pallet_foo:

#[pallet::genesis_config]
#[derive(DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
	pub some_integer: u32,
	pub some_enum: FooEnum,
	pub some_struct: FooStruct,
	#[serde(skip)]
	pub _phantom: PhantomData<T>,
}

Note that serde attributes can be used to control how the data structures are stored into JSON. In the following example, the [sp_core::bytes] function is used to serialize the values field.

#[derive(Default, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct SomeFooData2 {
	#[serde(default, with = "sp_core::bytes")]
	pub values: Vec<u8>,
}

Please note that fields of GenesisConfig may not be directly mapped to storage items. In the following example, the initial struct fields are used to compute (sum) the value that will be stored in the state as ProcessedEnumValue:

#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
	/// The build method that indirectly maps an initial config values into the storage items.
	fn build(&self) {
		let processed_value: u64 = match &self.some_enum {
			FooEnum::Data0 => 0,
			FooEnum::Data1(v) => (v.a + v.b).into(),
			FooEnum::Data2(v) => v.values.iter().map(|v| *v as u64).sum(),
		};
		ProcessedEnumValue::<T>::set(Some(processed_value));
		SomeInteger::<T>::set(Some(self.some_integer));
	}
}

§GenesisConfig for runtimes

The runtime genesis config struct consists of configs for every pallet. For the demonstration runtime used in this guide, it consists of SystemConfig, BarConfig, and FooConfig. This structure was automatically generated by a macro and it can be sneak-peeked here: RuntimeGenesisConfig. For further reading on generated runtime types, refer to frame_runtime_types.

The macro automatically adds an attribute that renames all the fields to camelCase. It is a good practice to add it to nested structures too, to have the naming of the JSON keys consistent across the chain-spec file.

§Default for GenesisConfig

GenesisConfig of all pallets must implement the Default trait. These are aggregated into the runtime’s RuntimeGenesisConfig’s Default.

The default value of RuntimeGenesisConfig can be queried by the GenesisBuilder::get_preset function provided by the runtime with id:None.

A default value for RuntimeGenesisConfig usually is not operational. This is because for some pallets it is not possible to define good defaults (e.g. an initial set of authorities).

A default value is a base upon which a patch for GenesisConfig is applied. A good description of how it exactly works is provided in get_storage_for_patch (and also in GenesisBuilder::get_preset). A patch can be provided as an external file (manually created) or as a built-in runtime preset. More info on presets is in the material to follow.

§Implementing GenesisBuilder for runtime

The runtime exposes a dedicated runtime API for interacting with its genesis config: sp_genesis_builder::GenesisBuilder. The implementation shall be provided within the sp_api::impl_runtime_apis macro, typically making use of some helpers provided: build_state, get_preset. A typical implementation of sp_genesis_builder::GenesisBuilder looks as follows:

impl_runtime_apis! {
	impl sp_genesis_builder::GenesisBuilder<Block> for Runtime {
		fn build_state(config: Vec<u8>) -> sp_genesis_builder::Result {
			build_state::<RuntimeGenesisConfig>(config)
		}

		fn get_preset(id: &Option<sp_genesis_builder::PresetId>) -> Option<Vec<u8>> {
			get_preset::<RuntimeGenesisConfig>(id, get_builtin_preset)
		}

		fn preset_names() -> Vec<sp_genesis_builder::PresetId> {
			vec![
				PresetId::from(PRESET_1),
				PresetId::from(PRESET_2),
				PresetId::from(PRESET_3),
				PresetId::from(PRESET_4),
				PresetId::from(PRESET_INVALID)
			]
		}
	}

	impl apis::Core<Block> for Runtime {
		fn version() -> RuntimeVersion { VERSION }
		fn execute_block(_: Block) { }
		fn initialize_block(_: &Header) -> ExtrinsicInclusionMode { ExtrinsicInclusionMode::default() }
	}
}

Please note that two functions are customized: preset_names and get_preset. The first one just provides a Vec of the names of supported presets, while the latter delegates the call to a function that maps the name to an actual preset: chain_spec_guide_runtime::presets::get_builtin_preset

pub fn get_builtin_preset(id: &sp_genesis_builder::PresetId) -> Option<alloc::vec::Vec<u8>> {
	let preset = match id.as_ref() {
		PRESET_1 => preset_1(),
		PRESET_2 => preset_2(),
		PRESET_3 => preset_3(),
		PRESET_4 => preset_4(),
		PRESET_INVALID => preset_invalid(),
		_ => return None,
	};

	Some(
		to_string(&preset)
			.expect("serialization to json is expected to work. qed.")
			.into_bytes(),
	)
}

§Genesis state presets for runtime

The runtime may provide many flavors of initial genesis state. This may be useful for predefined testing networks, local development, or CI integration tests. Predefined genesis state may contain a list of pre-funded accounts, predefined authorities for consensus, sudo key, and many others useful for testing.

Internally, presets can be provided in a number of ways:

fn preset_2() -> Value {
	build_struct_json_patch!(RuntimeGenesisConfig {
		foo: FooConfig {
			some_integer: 200,
			some_enum: FooEnum::Data2(SomeFooData2 { values: vec![0x0c, 0x10] })
		},
		bar: BarConfig { initial_account: Some(AccountKeyring::Ferdie.public().into()) },
	})
}
  • JSON using runtime types to serialize values:
fn preset_3() -> Value {
	json!({
		"bar": {
			"initialAccount": AccountKeyring::Alice.public().to_ss58check(),
		},
		"foo": {
			"someEnum": FooEnum::Data1(
				SomeFooData1 {
					a: 12,
					b: 16
				}
			),
			"someInteger": 300
		},
	})
}
  • JSON in string form:
fn preset_1() -> Value {
	json!({
		"bar": {
			"initialAccount": "5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL",
		},
		"foo": {
			"someEnum": {
				"Data2": {
					"values": "0x0c0f"
				}
			},
			"someStruct" : {
				"fieldA": 10,
				"fieldB": 20
			},
			"someInteger": 100
		},
	})
}

It is worth noting that a preset does not have to be the full RuntimeGenesisConfig, in that sense that it does not have to contain all the keys of the struct. The preset is actually a JSON patch that will be merged with the default value of RuntimeGenesisConfig. This approach should simplify maintenance of built-in presets. The following example illustrates a runtime genesis config patch with a single key built using build_struct_json_patch macro:

pub fn preset_4() -> Value {
	build_struct_json_patch!(RuntimeGenesisConfig {
		foo: FooConfig { some_enum: FooEnum::Data2(SomeFooData2 { values: vec![0x0c, 0x10] }) },
	})
}

This results in the following JSON blob:

#[test]
fn preset_4_json() {
	assert_eq!(
		chain_spec_guide_runtime::presets::preset_4(),
		json!({
			"foo": {
				"someEnum": {
					"Data2": {
						"values": "0x0c10"
					}
				},
			},
		})
	);
}

§Note on the importance of testing presets

It is recommended to always test presets by adding tests that convert the preset into the raw storage. Converting to raw storage involves the deserialization of the provided JSON blob, which enforces the verification of the preset. The following code shows one of the approaches that can be taken for testing:

#[test]
fn check_presets() {
	let builder = sc_chain_spec::GenesisConfigBuilderRuntimeCaller::<()>::new(
		crate::WASM_BINARY.expect("wasm binary shall exists"),
	);
	assert!(builder.get_storage_for_named_preset(Some(&PRESET_1.to_string())).is_ok());
	assert!(builder.get_storage_for_named_preset(Some(&PRESET_2.to_string())).is_ok());
	assert!(builder.get_storage_for_named_preset(Some(&PRESET_3.to_string())).is_ok());
	assert!(builder.get_storage_for_named_preset(Some(&PRESET_4.to_string())).is_ok());
}

§Note on the importance of using the deny_unknown_fields attribute

It is worth noting that when manually building preset JSON blobs it is easy to make a hard-to-spot mistake, as in the following example (FooStruct does not contain fieldC):

fn preset_invalid() -> Value {
	json!({
		"foo": {
			"someStruct": {
				"fieldC": 5
			},
		},
	})
}

Even though preset_invalid contains a key that does not exist, the deserialization of the JSON blob does not fail. The misspelling is silently ignored due to the lack of the deny_unknown_fields attribute on the FooStruct struct, which is internally used in GenesisConfig.

#[test]
fn invalid_preset_works() {
	let builder = sc_chain_spec::GenesisConfigBuilderRuntimeCaller::<()>::new(
		crate::WASM_BINARY.expect("wasm binary shall exists"),
	);
	// Even though a preset contains invalid_key, conversion to raw storage does not fail. This is
	// because the [`FooStruct`] structure is not annotated with `deny_unknown_fields` [`serde`]
	// attribute.
	// This may lead to hard to debug problems, that's why using ['deny_unknown_fields'] is
	// recommended.
	assert!(builder.get_storage_for_named_preset(Some(&PRESET_INVALID.to_string())).is_ok());
}

To avoid this problem build_struct_json_patch macro shall be used whenever possible (it internally instantiates the struct before serializang it JSON blob, so all unknown fields shall be caught at compilation time).

§Runtime GenesisConfig raw format

A raw format of genesis config contains just the state’s keys and values as they are stored in the storage. This format is used to directly initialize the genesis storage. This format is useful for long-term running chains, where the GenesisConfig structure for pallets may be evolving over time. The JSON representation created at some point in time may no longer be deserializable in the future, making a chain specification useless. The raw format is recommended for production chains.

For a detailed description of how the raw format is built, please refer to chain-spec-raw-genesis. Plain and corresponding raw examples of chain-spec are given in chain-spec-examples. The chain_spec_builder util supports building the raw storage.

§Interacting with the tool

The chain_spec_builder util allows interaction with the runtime in order to list or display presets and build the chain specification file. It is possible to use the tool with the demonstration runtime. To build the required packages, just run the following command:

cargo build -p staging-chain-spec-builder -p chain-spec-guide-runtime --release

The chain-spec-builder util can also be installed with cargo install:

cargo install staging-chain-spec-builder
cargo build -p chain-spec-guide-runtime --release

Here are some examples in the form of rust tests:

§Listing available preset names:

#[test]
fn list_presets() {
	let output = Command::new(get_chain_spec_builder_path())
		.arg("list-presets")
		.arg("-r")
		.arg(WASM_FILE_PATH)
		.output()
		.expect("Failed to execute command");

	let output: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();

	let expected_output = json!({
		"presets":[
			"preset_1",
			"preset_2",
			"preset_3",
			"preset_4",
			"preset_invalid"
		]
	});
	assert_eq!(output, expected_output, "Output did not match expected");
}

§Displaying preset with given name

#[test]
fn get_preset() {
	let output = Command::new(get_chain_spec_builder_path())
		.arg("display-preset")
		.arg("-r")
		.arg(WASM_FILE_PATH)
		.arg("-p")
		.arg("preset_2")
		.output()
		.expect("Failed to execute command");

	let output: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();

	//note: copy of chain_spec_guide_runtime::preset_2
	let expected_output = json!({
		"bar": {
			"initialAccount": "5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL",
		},
		"foo": {
			"someEnum": {
				"Data2": {
					"values": "0x0c10"
				}
			},
			"someInteger": 200
		},
	});
	assert_eq!(output, expected_output, "Output did not match expected");
}

§Building a solo chain-spec (the default) using given preset

#[test]
fn generate_chain_spec() {
	let output = Command::new(get_chain_spec_builder_path())
		.arg("-c")
		.arg("/dev/stdout")
		.arg("create")
		.arg("-r")
		.arg(WASM_FILE_PATH)
		.arg("named-preset")
		.arg("preset_2")
		.output()
		.expect("Failed to execute command");

	let mut output: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();

	//remove code field for better readability
	if let Some(code) = output["genesis"]["runtimeGenesis"].as_object_mut().unwrap().get_mut("code")
	{
		*code = Value::String("0x123".to_string());
	}

	let expected_output = json!({
	  "name": "Custom",
	  "id": "custom",
	  "chainType": "Live",
	  "bootNodes": [],
	  "telemetryEndpoints": null,
	  "protocolId": null,
	  "properties": { "tokenDecimals": 12, "tokenSymbol": "UNIT" },
	  "codeSubstitutes": {},
	  "genesis": {
		"runtimeGenesis": {
		  "code": "0x123",
		  "patch": {
			"bar": {
			  "initialAccount": "5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL"
			},
			"foo": {
			  "someEnum": {
				"Data2": {
				  "values": "0x0c10"
				}
			  },
			  "someInteger": 200
			}
		  }
		}
	  }
	});
	assert_eq!(output, expected_output, "Output did not match expected");
}

§Building a parachain chain-spec using given preset

#[test]
fn generate_para_chain_spec() {
	let output = Command::new(get_chain_spec_builder_path())
		.arg("-c")
		.arg("/dev/stdout")
		.arg("create")
		.arg("-c")
		.arg("polkadot")
		.arg("-p")
		.arg("1000")
		.arg("-r")
		.arg(WASM_FILE_PATH)
		.arg("named-preset")
		.arg("preset_2")
		.output()
		.expect("Failed to execute command");

	let mut output: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();

	//remove code field for better readability
	if let Some(code) = output["genesis"]["runtimeGenesis"].as_object_mut().unwrap().get_mut("code")
	{
		*code = Value::String("0x123".to_string());
	}

	let expected_output = json!({
	  "name": "Custom",
	  "id": "custom",
	  "chainType": "Live",
	  "bootNodes": [],
	  "telemetryEndpoints": null,
	  "protocolId": null,
	  "relay_chain": "polkadot",
	  "para_id": 1000,
	  "properties": { "tokenDecimals": 12, "tokenSymbol": "UNIT" },
	  "codeSubstitutes": {},
	  "genesis": {
		"runtimeGenesis": {
		  "code": "0x123",
		  "patch": {
			"bar": {
			  "initialAccount": "5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL"
			},
			"foo": {
			  "someEnum": {
				"Data2": {
				  "values": "0x0c10"
				}
			  },
			  "someInteger": 200
			}
		  }
		}
	  }
	});
	assert_eq!(output, expected_output, "Output did not match expected");
}