Construct Runtime
How to use the slides - Full screen (new tab)
construct_runtime!
and Testing 🔨
Part 1: Runtime Construction
Pallet <=> Runtime
A runtime is really ✌️ things:
- A struct that implements
Config
of all pallets. - A type that helps
Executive
implementRuntimeApis
.
---v
Pallet <=> Runtime
We build a runtime, using construct_runtime
, typically twice:
- Per pallet, there is a mock runtime.
- A real runtime elsewhere.
Note:
Benchmarking can then use both of these runtimes.
construct_runtime
: Runtime
type
#![allow(unused)] fn main() { frame_support::construct_runtime!( pub struct Runtime { System: frame_system, Timestamp: pallet_timestamp, Balances: pallet_balances, Aura: pallet_aura, Dpos: pallet_dpos, } ); }
---v
Runtime
type
- It implements A LOT OF STUFF!
- But most importantly, the
Config
trait of all of your pallets 🫵🏻.
#![allow(unused)] fn main() { impl frame_system::Config for Runtime { .. } impl pallet_timestamp::Config for Runtime { .. } impl pallet_dpos::Config for Runtime { .. } }
---v
<T: Config>
==> Runtime
Anywhere in your pallet code that you have
<T: Config>
can now be replaced withRuntime
.
#![allow(unused)] fn main() { // a normal pub function defined in frame_system::Pallet::<Runtime>::block_number(); // a storage getter of a map. frame_system::Pallet::<Runtime>::account(42u32); // A storage type. frame_system::Account::<Runtime>::get(42u32); }
construct_runtime
: Pallet List
#![allow(unused)] fn main() { frame_support::construct_runtime!( pub struct Runtime { System: frame_system, Timestamp: pallet_timestamp, Balances: pallet_balances, Aura: pallet_aura, Dpos: pallet_dpos, <NameYouChoose>: path_to_crate, } ); }
---v
Pallet List
- Crucially, under the hood, this generates:
#![allow(unused)] fn main() { type System = frame_system::Pallet<Runtime>; type Balances = pallet_balances::Pallet<Runtime>; .. type DPos = pallet_dpos::Pallet<Runtime>; }
- Recall that
Runtime
implements<T: Config>
of all pallets.
---v
Pallet List
#![allow(unused)] fn main() { frame_system::Pallet::<Runtime>::block_number(); // 🤮 System::block_number(); // 🥳 frame_system::Pallet::<Runtime>::account(42u32); // 🤮 System::account(42u32); // 🥳 }
---v
Pallet List
- Next crucial piece of information that is generated is:
#![allow(unused)] fn main() { type AllPallets = (System, Balances, ..., Dpos); }
- This is used in
Executive
to dispatch pallet hooks.
#![allow(unused)] fn main() { <AllPallets as OnInitialize>::on_initialize(); <AllPallets as OnInitialize>::on_finalize(); }
Notes:
Question: What will be the order of fn on_initialize()
?
There's also type AllPalletsWithoutSystem
and some other variants that are no longer
---v
Pallet List + Outer Enums
-
Generates some outer types:
RuntimeCall
RuntimeEvent
RuntimeOrigin
RuntimeGenesisConfig
Notes:
See the lecture on individual item, and the "Outer Enum" lecture.
---v
Pallet List: RuntimeCall
Example
#![allow(unused)] fn main() { // somewhere in your pallet, called `my_pallet` #[pallet::call] impl<T: Config> Pallet<T> { fn transfer(origin: OriginFor<T>, from: T::AccountId, to: T::AccountId, amount: u128); fn update_runtime(origin: OriginFor<T>, new_code: Vec<u8>); } }
#![allow(unused)] fn main() { // expanded in your pallet enum Call { transfer { from: T::AccountId, to: T::AccountId, amount: u128 }, update_runtime { new_code: Vec<u8> }, } }
#![allow(unused)] fn main() { // in your outer runtime enum RuntimeCall { System(frame_system::Call), MyPallet(my_pallet::Call), } }
---v
Pallet List: Pallet Parts
#![allow(unused)] fn main() { frame_support::construct_runtime!( pub struct Runtime { System: frame_system::{Pallet, Call, Config, Storage, Event<T>}, Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>}, Dpos: pallet_dpos, } ); }
- Omitting them will exclude them from the metadata, or the "outer/runtime types"
---v
Pallet List: Pallet Index
#![allow(unused)] fn main() { frame_support::construct_runtime!( pub struct Runtime { System: frame_system::{Pallet, Call, Config, Storage, Event<T>} = 1, Balances: pallet_balances = 0, Dpos: pallet_dpos = 2, } ); }
construct_runtime
: Final Thoughts
- Order in the
construct_runtime
matters! - Recall
integrity_test()
is called uponconstruct_runtime
.
test mock::__construct_runtime_integrity_test::runtime_integrity_tests ... ok
---v
Preview
Of the next potential syntax:
#![allow(unused)] fn main() { #[frame::construct_runtime] mod runtime { #[frame::runtime] pub struct Runtime; #[frame::executive] pub struct Executive; #[frame::pallets] #[derive(RuntimeGenesisConfig, RuntimeCall, RuntimeOrigin)] pub type AllPallets = ( System = frame_system = 0, BalancesFoo = pallet_balances = 1, BalancesBar = pallet_balances = 2, Staking = pallet_staking = 42, ); } }
Notes:
See: https://github.com/paritytech/polkadot-sdk/issues/232
Part 2: Testing
Testing and Mocks
A test requires a mock runtime, so we need to do a full construct_runtime
😱
.. but luckily, most types can be mocked 😮💨
---v
Testing and Mocks
u32
account id.u128
balance.u32
block number.- ...
Testing: Get<_>
- Next, we want to supply some value to those
Get<_>
associated types.
#![allow(unused)] fn main() { #[pallet::config] pub trait Config: frame_system::Config { type MaxVoters: Get<u32>; } }
---v
Testing: Get<_>
#![allow(unused)] fn main() { parameter_types! { pub const MyMaxVoters: u32 = 16; } }
#![allow(unused)] fn main() { impl pallet_template::Config for Runtime { type MaxVoters = MyMaxVoters; } }
---v
Testing: Get<_>
- Or, if your value is always constant:
#![allow(unused)] fn main() { impl pallet_dpos::Config for Runtime { type MaxVoters = frame_support::traits::ConstU32<16>; } }
---v
Testing: Get<_>
- Or, if you want to torture yourself:
#![allow(unused)] fn main() { pub struct MyMaxVoters; impl Get<u32> for MyMaxVoters { fn get() -> u32 { 100 } } impl pallet_dpos::Config for Runtime { type MaxVoters = MyMaxVoters; } }
Testing: Genesis and Builder
- Next, if you want to feed some data into your pallet's genesis state, we must first setup the genesis config correctly.
#![allow(unused)] fn main() { #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig<T: Config> { pub voters: Vec<(T::AccountId, Option<Vote>)>, } #[pallet::genesis_build] impl<T: Config> BuildGenesisConfig for GenesisConfig<T> { fn build(&self) { for (voter, maybe_vote) in &self.voters { // do stuff. } } } }
---v
Testing and Mocks: Genesis and Builder
- Then, we build a builder pattern to construct the genesis config.
#![allow(unused)] fn main() { #[derive(Default)] pub struct Builder { pub voters: Vec<(u64, Option<Vote>)>, } }
#![allow(unused)] fn main() { impl Builder { pub fn add_voter(mut self, who: u64) -> Self { self.voters.push((who, None)); self } } }
---v
Testing and Mocks: Genesis and Builder
- Finally:
#![allow(unused)] fn main() { impl Builder { pub fn build(self) -> TestExternalities { let system = frame_system::GenesisConfig::<Runtime>::default(); let template_module = crate::GenesisConfig { voters: self.voters, ..Default::default() }; RuntimeGenesisConfig { system, template_module }.build_storage().unwrap().into() } pub fn build_and_execute(self, f: impl FnOnce()) { let mut ext = self.build(); ext.execute_with(f); // any post checks can come here. } } }
---v
Testing and Mocks
- Finally, this allows you to write a test like this:
#![allow(unused)] fn main() { #[test] fn test_stuff() { let mut ext = Builder::default() .add_voter_with_vote(2, Vote::Aye) .add_voter(3) build_and_execute(|| { // do stuff }); } }
Testing: static parameter_types!
- What if you want to change that
MyMaxVoters
?
#![allow(unused)] fn main() { parameter_types! { pub static MyMaxVoters: u32 = 100; } }
#![allow(unused)] fn main() { MyMaxVoters::set(200); MyMaxVoters::get(); }
Test ing: Progressing Blocks
- Often times, in your test, you want mimic the progression of an empty block.
- De-nada! We can fake everything in tests 🤠
---v
Progressing Blocks
#![allow(unused)] fn main() { pub fn next_block() { let now = System::block_number(); Dpos::on_finalize(now); System::on_finalize(now); System::set_block_number(now + 1); System::on_initialize(now + 1) Dpos::on_initialize(now + 1); } }
---v
Progressing Blocks
#![allow(unused)] fn main() { pub fn next_block() { let now = System::block_number(); AllPallets::on_finalize(now); System::set_block_number(now + 1); AllPallets::on_initialize(now + 1) } }
---v
Progressing Blocks
#![allow(unused)] fn main() { ```rust #[test] fn test() { let mut ext = Builder::default() .add_validator(1) .set_minimum_delegation(200) .build(); ext.execute_with(|| { // initial stuff next_block(); // dispatch some call assert!(some_condition); next_block(); // repeat.. }); } }
---
## Additional Resources 😋
> Check speaker notes (click "s" 😉)
Notes:
- This PR was actually an outcome Cambridge PBA: <https://github.com/paritytech/substrate/pull/11932>
- <https://github.com/paritytech/substrate/pull/11818>
- <https://github.com/paritytech/substrate/pull/10043>
- On usage of macros un Substrate: <https://github.com/paritytech/substrate/issues/12331>
- Discussion on advance testing: <https://forum.polkadot.network/t/testing-complex-frame-pallets-discussion-tools/356>
- Reserve topic: Reading events.
- Reserve-topic: try-state.
### Original Lecture Script
this is your bridge from a pallet into a runtime.
a runtime amalgamator is composed of the following:
1. all pallet's `Config` implemented by a `struct Runtime`;
1. construct `Executive` and use it to implement all the runtime APIs
1. Optionally, some boilerplate to setup benchmarking.
1. invoke `construct_runtime!`.
1. Alias for each pallet.
The `construct_runtime!` itself does a few things under the hood:
1. crate `struct Runtime`.
1. amalgamate `enum RuntimeCall`; // passed inwards to some pallets that want to store calls.
1. amalgamate `enum RuntimeEvent`; // passed inwards to all pallets.
1. amalgamate `enum RuntimeOrigin` (this is a fixed struct, not an amalgamation);
1. Create a very important type alias:
- `type AllPallets` / `type AllPalletsWithoutSystem`
1. run `integrity_test()`.
> Note that there is no such thing as `RuntimeError`. Errors are not amalgamated, they just are. This should be in the error lecture.
- Ordering in `construct_runtime` matters.
- Pallet parts can be optional in `construct_runtime!`.