diff --git a/Cargo.lock b/Cargo.lock index 9431a1a02fdf7..09a52649a12a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4718,8 +4718,11 @@ dependencies = [ "cumulus-primitives-proof-size-hostfunction", "cumulus-test-client", "cumulus-test-relay-sproof-builder", + "derive-where", + "docify", "environmental", "frame-benchmarking", + "frame-executive", "frame-support", "frame-system", "futures", @@ -4730,6 +4733,7 @@ dependencies = [ "pallet-message-queue", "parity-scale-codec", "polkadot-parachain-primitives", + "polkadot-primitives", "polkadot-runtime-parachains", "rand 0.8.5", "rstest", diff --git a/cumulus/pallets/parachain-system/Cargo.toml b/cumulus/pallets/parachain-system/Cargo.toml index 7c111579f0672..50c262ad8c733 100644 --- a/cumulus/pallets/parachain-system/Cargo.toml +++ b/cumulus/pallets/parachain-system/Cargo.toml @@ -15,6 +15,7 @@ workspace = true array-bytes = { workspace = true } bytes = { workspace = true } codec = { features = ["derive"], workspace = true } +derive-where = { workspace = true } environmental = { workspace = true } hashbrown = { workspace = true } impl-trait-for-tuples = { workspace = true } @@ -40,6 +41,7 @@ sp-version = { workspace = true } # Polkadot polkadot-parachain-primitives = { features = ["wasm-api"], workspace = true } +polkadot-primitives = { workspace = true } polkadot-runtime-parachains = { workspace = true } xcm = { workspace = true } xcm-builder = { workspace = true } @@ -50,6 +52,9 @@ cumulus-primitives-core = { workspace = true } cumulus-primitives-parachain-inherent = { workspace = true } cumulus-primitives-proof-size-hostfunction = { workspace = true } +# For building docs +docify = { workspace = true } + [dev-dependencies] assert_matches = { workspace = true } futures = { workspace = true } @@ -58,8 +63,8 @@ rand = { workspace = true, default-features = true } rstest = { workspace = true } trie-standardmap = { workspace = true } - # Substrate +frame-executive = { workspace = true } sc-consensus = { workspace = true } sp-api = { workspace = true, default-features = true } sp-consensus-slots = { workspace = true, default-features = true } @@ -67,6 +72,7 @@ sp-crypto-hashing = { workspace = true, default-features = true } sp-keyring = { workspace = true, default-features = true } sp-tracing = { workspace = true, default-features = true } sp-version = { workspace = true, default-features = true } + # Cumulus cumulus-test-client = { workspace = true } cumulus-test-relay-sproof-builder = { workspace = true, default-features = true } @@ -87,6 +93,7 @@ std = [ "log/std", "pallet-message-queue/std", "polkadot-parachain-primitives/std", + "polkadot-primitives/std", "polkadot-runtime-parachains/std", "scale-info/std", "sp-consensus-babe/std", diff --git a/cumulus/pallets/parachain-system/src/benchmarking.rs b/cumulus/pallets/parachain-system/src/benchmarking.rs index c3d59e82255a3..ebe3c66785f52 100644 --- a/cumulus/pallets/parachain-system/src/benchmarking.rs +++ b/cumulus/pallets/parachain-system/src/benchmarking.rs @@ -20,12 +20,33 @@ #![cfg(feature = "runtime-benchmarks")] use super::*; -use crate::parachain_inherent::InboundDownwardMessages; -use cumulus_primitives_core::{relay_chain::Hash as RelayHash, InboundDownwardMessage}; +use crate::{ + block_weight::{ + BlockWeightMode, DynamicMaxBlockWeight, MaxParachainBlockWeight, FULL_CORE_WEIGHT, + }, + parachain_inherent::InboundDownwardMessages, +}; +use cumulus_primitives_core::{ + relay_chain::Hash as RelayHash, BundleInfo, CoreInfo, InboundDownwardMessage, +}; use frame_benchmarking::v2::*; -use sp_runtime::traits::BlakeTwo256; +use frame_support::{ + dispatch::{DispatchInfo, PostDispatchInfo}, + weights::constants::WEIGHT_REF_TIME_PER_SECOND, +}; +use frame_system::RawOrigin; +use sp_core::ConstU32; +use sp_runtime::traits::{BlakeTwo256, DispatchTransaction, Dispatchable}; -#[benchmarks] +fn has_use_full_core_digest() -> bool { + let digest = frame_system::Pallet::::digest(); + CumulusDigestItem::contains_use_full_core(&digest) +} + +#[benchmarks(where + T: Send + Sync, + T::RuntimeCall: Dispatchable, +)] mod benchmarks { use super::*; @@ -64,6 +85,184 @@ mod benchmarks { head } + /// The worst-case scenario for the block weight transaction extension. + /// + /// Before executing an extrinsic `FractionOfCore` is set, changed to `PotentialFullCore` and + /// post dispatch switches to `FullCore`. + #[benchmark] + fn block_weight_tx_extension_max_weight() -> Result<(), BenchmarkError> { + let caller = account("caller", 0, 0); + + frame_system::Pallet::::note_inherents_applied(); + + frame_system::Pallet::::set_extrinsic_index(1); + + frame_system::Pallet::::deposit_log( + BundleInfo { index: 0, maybe_last: false }.to_digest_item(), + ); + frame_system::Pallet::::deposit_log( + CoreInfo { + selector: 0.into(), + claim_queue_offset: 0.into(), + number_of_cores: 1.into(), + } + .to_digest_item(), + ); + let target_weight = MaxParachainBlockWeight::>::target_block_weight(); + + let info = DispatchInfo { + // The weight needs to be more than the target weight. + call_weight: target_weight + .saturating_add(Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 0)), + extension_weight: Weight::zero(), + class: DispatchClass::Normal, + ..Default::default() + }; + let call: T::RuntimeCall = frame_system::Call::remark { remark: vec![] }.into(); + let post_info = PostDispatchInfo { actual_weight: None, pays_fee: Default::default() }; + let len = 0_usize; + + crate::BlockWeightMode::::put(BlockWeightMode::fraction_of_core(None)); + + let ext = DynamicMaxBlockWeight::>::new(()); + + #[block] + { + ext.test_run(RawOrigin::Signed(caller).into(), &call, &info, len, 0, |_| { + // Normally this is done by `CheckWeight` + frame_system::Pallet::::register_extra_weight_unchecked( + info.call_weight, + DispatchClass::Normal, + ); + Ok(post_info) + }) + .unwrap() + .unwrap(); + } + + assert_eq!(crate::BlockWeightMode::::get().unwrap(), BlockWeightMode::full_core()); + assert!(has_use_full_core_digest::()); + assert_eq!(MaxParachainBlockWeight::>::get(), FULL_CORE_WEIGHT); + + Ok(()) + } + + /// A benchmark that assumes that an extrinsic was executed with `FractionOfCore` set. + #[benchmark] + fn block_weight_tx_extension_stays_fraction_of_core() -> Result<(), BenchmarkError> { + let caller = account("caller", 0, 0); + + frame_system::Pallet::::note_inherents_applied(); + + frame_system::Pallet::::set_extrinsic_index(1); + + frame_system::Pallet::::deposit_log( + BundleInfo { index: 0, maybe_last: false }.to_digest_item(), + ); + frame_system::Pallet::::deposit_log( + CoreInfo { + selector: 0.into(), + claim_queue_offset: 0.into(), + number_of_cores: 1.into(), + } + .to_digest_item(), + ); + let target_weight = MaxParachainBlockWeight::>::target_block_weight(); + + let info = DispatchInfo { + call_weight: Weight::from_parts(1024, 1024), + extension_weight: Weight::zero(), + class: DispatchClass::Normal, + ..Default::default() + }; + let call: T::RuntimeCall = frame_system::Call::remark { remark: vec![] }.into(); + let post_info = PostDispatchInfo { actual_weight: None, pays_fee: Default::default() }; + let len = 0_usize; + + crate::BlockWeightMode::::put(BlockWeightMode::fraction_of_core(None)); + + let ext = DynamicMaxBlockWeight::>::new(()); + + #[block] + { + ext.test_run(RawOrigin::Signed(caller).into(), &call, &info, len, 0, |_| { + // Normally this is done by `CheckWeight` + frame_system::Pallet::::register_extra_weight_unchecked( + info.call_weight, + DispatchClass::Normal, + ); + Ok(post_info) + }) + .unwrap() + .unwrap(); + } + + assert_eq!( + crate::BlockWeightMode::::get().unwrap(), + BlockWeightMode::fraction_of_core(Some(1)) + ); + assert!(!has_use_full_core_digest::()); + assert_eq!(MaxParachainBlockWeight::>::get(), target_weight); + + Ok(()) + } + + /// A benchmark that assumes that `FullCore` was set already before executing an extrinsic. + #[benchmark] + fn block_weight_tx_extension_full_core() -> Result<(), BenchmarkError> { + let caller = account("caller", 0, 0); + + frame_system::Pallet::::set_block_number(1u32.into()); + + frame_system::Pallet::::note_inherents_applied(); + + frame_system::Pallet::::set_extrinsic_index(1); + + frame_system::Pallet::::deposit_log( + BundleInfo { index: 0, maybe_last: false }.to_digest_item(), + ); + frame_system::Pallet::::deposit_log( + CoreInfo { + selector: 0.into(), + claim_queue_offset: 0.into(), + number_of_cores: 1.into(), + } + .to_digest_item(), + ); + + let info = DispatchInfo { + call_weight: Weight::from_parts(1024, 1024), + extension_weight: Weight::zero(), + class: DispatchClass::Normal, + ..Default::default() + }; + let call: T::RuntimeCall = frame_system::Call::remark { remark: vec![] }.into(); + let post_info = PostDispatchInfo { actual_weight: None, pays_fee: Default::default() }; + let len = 0_usize; + + crate::BlockWeightMode::::put(BlockWeightMode::full_core()); + + let ext = DynamicMaxBlockWeight::>::new(()); + + #[block] + { + ext.test_run(RawOrigin::Signed(caller).into(), &call, &info, len, 0, |_| { + // Normally this is done by `CheckWeight` + frame_system::Pallet::::register_extra_weight_unchecked( + info.call_weight, + DispatchClass::Normal, + ); + Ok(post_info) + }) + .unwrap() + .unwrap(); + } + + assert_eq!(crate::BlockWeightMode::::get().unwrap(), BlockWeightMode::full_core()); + + Ok(()) + } + impl_benchmark_test_suite! { Pallet, crate::mock::new_test_ext(), diff --git a/cumulus/pallets/parachain-system/src/block_weight/mock.rs b/cumulus/pallets/parachain-system/src/block_weight/mock.rs new file mode 100644 index 0000000000000..ac1a9bcdd4845 --- /dev/null +++ b/cumulus/pallets/parachain-system/src/block_weight/mock.rs @@ -0,0 +1,465 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Cumulus. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{transaction_extension::DynamicMaxBlockWeight, *}; +use crate::{self as parachain_system, MessagingStateSnapshot, PreviousCoreCount}; +use codec::Compact; +use cumulus_primitives_core::{ + BundleInfo, ClaimQueueOffset, CoreInfo, CoreSelector, CumulusDigestItem, +}; +use frame_support::{ + construct_runtime, derive_impl, + dispatch::DispatchClass, + migrations::MultiStepMigrator, + parameter_types, + traits::PreInherents, + weights::{ + constants::{BlockExecutionWeight, ExtrinsicBaseWeight}, + Weight, + }, +}; +use frame_system::{limits::BlockWeights, CheckWeight}; +use polkadot_primitives::PersistedValidationData; +use sp_core::ConstU32; +use sp_io; +use sp_runtime::{ + generic::{self, UncheckedExtrinsic}, + testing::UintAuthorityId, + BuildStorage, Perbill, +}; + +const AVERAGE_ON_INITIALIZE_RATIO: Perbill = Perbill::from_percent(1); + +/// A simple call, which one doesn't matter. +pub const CALL: &RuntimeCall = + &RuntimeCall::System(frame_system::Call::set_heap_pages { pages: 0u64 }); + +pub type ExtrinsicOnlyOperational = UncheckedExtrinsic< + UintAuthorityId, + only_operational_runtime::RuntimeCall, + UintAuthorityId, + DynamicMaxBlockWeight< + RuntimeOnlyOperational, + CheckWeight, + ConstU32, + 10, + false, + >, +>; + +pub type Extrinsic = UncheckedExtrinsic< + UintAuthorityId, + RuntimeCall, + UintAuthorityId, + DynamicMaxBlockWeight, ConstU32>, +>; + +pub type Block = + generic::Block::Hashing>, Extrinsic>; + +pub type BlockOnlyOperational = generic::Block< + generic::Header::Hashing>, + ExtrinsicOnlyOperational, +>; + +pub const TARGET_BLOCK_RATE: u32 = 12; +pub const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75); + +#[docify::export(tx_extension_setup)] +pub type TxExtension = DynamicMaxBlockWeight< + Runtime, + // Here you need to set the other extensions that are required by your runtime... + ( + frame_system::AuthorizeCall, + frame_system::CheckNonZeroSender, + frame_system::CheckSpecVersion, + frame_system::CheckGenesis, + frame_system::CheckEra, + frame_system::CheckNonce, + frame_system::CheckWeight, + ), + ConstU32, +>; + +#[allow(dead_code)] +type NotDeadCode = TxExtension; + +#[docify::export_content(max_block_weight_setup)] +mod max_block_weight_setup { + use super::*; + + type MaximumBlockWeight = MaxParachainBlockWeight>; + + parameter_types! { + pub RuntimeBlockWeights: BlockWeights = BlockWeights::builder() + .base_block(BlockExecutionWeight::get()) + .for_class(DispatchClass::all(), |weights| { + weights.base_extrinsic = ExtrinsicBaseWeight::get(); + }) + .for_class(DispatchClass::Normal, |weights| { + weights.max_total = Some(MaximumBlockWeight::get() * NORMAL_DISPATCH_RATIO); + }) + .for_class(DispatchClass::Operational, |weights| { + weights.max_total = Some(MaximumBlockWeight::get()); + }) + .avg_block_initialization(AVERAGE_ON_INITIALIZE_RATIO) + .build_or_panic(); + } +} + +#[frame_support::pallet(dev_mode)] +pub mod test_pallet { + use super::*; + use frame_support::{ + dispatch::DispatchClass, + pallet_prelude::*, + weights::{constants::WEIGHT_REF_TIME_PER_SECOND, WeightMeter}, + }; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + crate::Config {} + + #[pallet::call] + impl Pallet { + /// A heavy call with Normal dispatch class that consumes significant weight. + #[pallet::weight((Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 1024 * 1024), DispatchClass::Normal))] + pub fn heavy_call_normal(_: OriginFor) -> DispatchResult { + Ok(()) + } + + /// A heavy call with Operational dispatch class that consumes significant weight. + #[pallet::weight((Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 1024 * 1024), DispatchClass::Operational))] + pub fn heavy_call_operational(_: OriginFor) -> DispatchResult { + Ok(()) + } + + /// A heavy call with Operational dispatch class that consumes significant weight. + #[pallet::weight((Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 1024 * 1024), DispatchClass::Mandatory))] + pub fn heavy_call_mandatory(_: OriginFor) -> DispatchResult { + Ok(()) + } + + #[pallet::weight((_weight.clone(), DispatchClass::Normal))] + pub fn use_weight(_: OriginFor, _weight: Weight) -> DispatchResult { + Ok(()) + } + } + + #[pallet::inherent] + impl ProvideInherent for Pallet { + type Call = Call; + type Error = sp_inherents::MakeFatalError<()>; + const INHERENT_IDENTIFIER: InherentIdentifier = *b"testtest"; + + fn create_inherent(_data: &InherentData) -> Option { + None + } + + fn is_inherent(call: &Self::Call) -> bool { + matches!(call, Call::heavy_call_mandatory {}) + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_idle(_block: BlockNumberFor, limit: Weight) -> Weight { + if let Some(max) = OnIdleMaxLeftWeight::get() { + assert!(limit.all_lte(max)); + } + + Weight::zero() + } + + fn on_poll(_n: BlockNumberFor, weight: &mut WeightMeter) { + if let Some(max) = OnPollMaxLeftWeight::get() { + assert!(weight.remaining().all_lte(max)); + } + } + } + + #[pallet::validate_unsigned] + impl sp_runtime::traits::ValidateUnsigned for Pallet { + type Call = Call; + fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { + Ok(ValidTransaction { + priority: u64::max_value(), + requires: Vec::new(), + provides: vec![call.encode()], + longevity: TransactionLongevity::max_value(), + propagate: true, + }) + } + } +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +#[docify::export(pre_inherents_setup)] +impl frame_system::Config for Runtime { + // Setup the block weight. + type BlockWeights = max_block_weight_setup::RuntimeBlockWeights; + // Set the `PreInherents` hook. + type PreInherents = DynamicMaxBlockWeightHooks>; + + // Just required to make it compile, but not that important for this example here. + type Block = Block; + type OnSetCode = crate::ParachainSetCode; + type AccountId = u64; + type Lookup = UintAuthorityId; + // Rest of the types are omitted here. +} + +impl crate::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type OnSystemEvent = (); + type SelfParaId = (); + type OutboundXcmpMessageSource = (); + type DmpQueue = (); + type ReservedDmpWeight = (); + type XcmpMessageHandler = (); + type ReservedXcmpWeight = (); + type CheckAssociatedRelayNumber = crate::RelayNumberStrictlyIncreases; + type WeightInfo = (); + type ConsensusHook = crate::ExpectParentIncluded; + type RelayParentOffset = (); +} + +impl test_pallet::Config for Runtime {} + +construct_runtime!( + pub enum Runtime { + System: frame_system, + ParachainSystem: parachain_system, + TestPallet: test_pallet, + } +); + +parameter_types! { + pub static MbmOngoing: bool = false; + pub static OnIdleMaxLeftWeight: Option = None; + pub static OnPollMaxLeftWeight: Option = None; +} + +pub struct Migrator; + +impl MultiStepMigrator for Migrator { + fn ongoing() -> bool { + MbmOngoing::get() + } + + fn step() -> Weight { + Weight::zero() + } +} + +pub mod only_operational_runtime { + use super::{BlockOnlyOperational, Migrator}; + use crate::block_weight::DynamicMaxBlockWeightHooks; + use frame_support::{construct_runtime, derive_impl}; + use sp_core::ConstU32; + use sp_runtime::testing::UintAuthorityId; + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] + impl frame_system::Config for RuntimeOnlyOperational { + type BlockWeights = super::max_block_weight_setup::RuntimeBlockWeights; + type PreInherents = + DynamicMaxBlockWeightHooks>; + type Block = BlockOnlyOperational; + type OnSetCode = crate::ParachainSetCode; + type AccountId = u64; + type Lookup = UintAuthorityId; + type MultiBlockMigrator = Migrator; + } + + impl crate::Config for RuntimeOnlyOperational { + type RuntimeEvent = RuntimeEvent; + type OnSystemEvent = (); + type SelfParaId = (); + type OutboundXcmpMessageSource = (); + type DmpQueue = (); + type ReservedDmpWeight = (); + type XcmpMessageHandler = (); + type ReservedXcmpWeight = (); + type CheckAssociatedRelayNumber = crate::RelayNumberStrictlyIncreases; + type WeightInfo = (); + type ConsensusHook = crate::ExpectParentIncluded; + type RelayParentOffset = (); + } + + impl super::test_pallet::Config for RuntimeOnlyOperational {} + + construct_runtime!( + pub enum RuntimeOnlyOperational { + System: frame_system, + ParachainSystem: super::parachain_system, + TestPallet: super::test_pallet, + } + ); +} + +pub use only_operational_runtime::{ + RuntimeCall as RuntimeCallOnlyOperational, RuntimeOnlyOperational, +}; + +/// Executive: handles dispatch to the various modules. +pub type Executive = frame_executive::Executive< + Runtime, + Block, + frame_system::ChainContext, + Runtime, + AllPalletsWithSystem, +>; + +/// Executive configured to only accept operational transaction to go over the limit. +pub type ExecutiveOnlyOperational = frame_executive::Executive< + RuntimeOnlyOperational, + BlockOnlyOperational, + frame_system::ChainContext, + RuntimeOnlyOperational, + only_operational_runtime::AllPalletsWithSystem, +>; + +/// Builder for test externalities +pub struct TestExtBuilder { + num_cores: Option, + bundle_index: Option, + bundle_maybe_last: bool, + previous_core_count: Option, +} + +impl Default for TestExtBuilder { + fn default() -> Self { + sp_tracing::try_init_simple(); + + Self { + num_cores: None, + bundle_index: None, + bundle_maybe_last: false, + previous_core_count: None, + } + } +} + +impl TestExtBuilder { + /// Create a new builder + pub fn new() -> Self { + Self::default() + } + + /// Set the number of cores + pub fn number_of_cores(mut self, num_cores: u16) -> Self { + self.num_cores = Some(num_cores); + self + } + + /// Set the `PreviousCoreCount` storage value. + pub fn previous_core_count(mut self, previous_core_count: u16) -> Self { + self.previous_core_count = Some(previous_core_count); + self + } + + /// Set this as the first block in the core (bundle index = 0) + pub fn first_block_in_core(mut self, is_first: bool) -> Self { + if is_first { + self.bundle_index = Some(0); + } else if self.bundle_index.is_none() { + // If not first and no bundle index set, default to index 1 + self.bundle_index = Some(1); + } + self + } + + /// Build the test externalities + pub fn build(self) -> sp_io::TestExternalities { + let storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let mut ext = sp_io::TestExternalities::from(storage); + + ext.execute_with(|| { + // Add core info if specified + if let Some(num_cores) = self.num_cores { + let core_info = CoreInfo { + selector: CoreSelector(0), + claim_queue_offset: ClaimQueueOffset(0), + number_of_cores: Compact(num_cores), + }; + let digest = CumulusDigestItem::CoreInfo(core_info).to_digest_item(); + frame_system::Pallet::::deposit_log(digest); + } + + // Add bundle info if specified + if let Some(bundle_index) = self.bundle_index { + let bundle_info = + BundleInfo { index: bundle_index, maybe_last: self.bundle_maybe_last }; + let digest = CumulusDigestItem::BundleInfo(bundle_info).to_digest_item(); + frame_system::Pallet::::deposit_log(digest); + } + + if let Some(previous_core_count) = self.previous_core_count { + PreviousCoreCount::::put(Compact(previous_core_count)); + } + }); + + ext + } +} + +/// Helper to check if UseFullCore digest was deposited +pub fn has_use_full_core_digest() -> bool { + let digest = frame_system::Pallet::::digest(); + CumulusDigestItem::contains_use_full_core(&digest) +} + +/// Helper to register weight as consumed (simulating on_initialize) +pub fn register_weight(weight: Weight, class: DispatchClass) { + frame_system::Pallet::::register_extra_weight_unchecked(weight, class); +} + +/// Emulates what happes after `initialize_block` finished. +pub fn initialize_block_finished() { + System::set_block_consumed_resources(Weight::zero(), 0); + System::note_finished_initialize(); + ::PreInherents::pre_inherents(); + System::note_inherents_applied(); +} + +/// Fakes the call to `set_validation_data`. +pub fn fake_set_validation_data() { + crate::ValidationData::::put(PersistedValidationData::default()); + crate::HostConfiguration::::put(cumulus_primitives_core::AbridgedHostConfiguration { + max_code_size: 2 * 1024 * 1024, + max_head_data_size: 1024 * 1024, + max_upward_queue_count: 8, + max_upward_queue_size: 1024, + max_upward_message_size: 256, + max_upward_message_num_per_candidate: 5, + hrmp_max_message_num_per_candidate: 5, + validation_upgrade_cooldown: 6, + validation_upgrade_delay: 6, + async_backing_params: cumulus_primitives_core::relay_chain::AsyncBackingParams { + allowed_ancestry_len: 0, + max_candidate_depth: 0, + }, + }); + crate::RelevantMessagingState::::put(MessagingStateSnapshot { + dmq_mqc_head: Default::default(), + relay_dispatch_queue_remaining_capacity: Default::default(), + ingress_channels: Vec::new(), + egress_channels: Vec::new(), + }); +} diff --git a/cumulus/pallets/parachain-system/src/block_weight/mod.rs b/cumulus/pallets/parachain-system/src/block_weight/mod.rs new file mode 100644 index 0000000000000..39c97cf9975a3 --- /dev/null +++ b/cumulus/pallets/parachain-system/src/block_weight/mod.rs @@ -0,0 +1,309 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Cumulus. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Provides functionality to dynamically calculate the block weight for a parachain. +//! +//! With block bundling, parachains are relative free to choose whatever block interval they want. +//! The block interval is the time between individual blocks. The available resources per block (max +//! block weight) depend on the number of cores allocated to the parachain on the relay chain. Each +//! relay chain cores provides an execution time of `2s` and a storage size of `10MiB`. Depending on +//! the desired number of blocks to produce, the resources need to be divided between the individual +//! blocks. With small blocks that do not have that many resources available, a problem may arises +//! for bigger transactions not fitting into blocks anymore, e.g. a runtime upgrade. For these cases +//! the weight of a block can be increased to use the weight of a full core. Only the first block of +//! a core is allowed to increase its weight to use the full core weight. In the case of the first +//! block using the full core weight, there will be no further block build on the same core. This is +//! signaled to the node by setting the [`CumulusDigestItem::UseFullCore`] digest item.` +//! +//! The [`MaxParachainBlockWeight`] provides a [`Get`] implementation that will return the max block +//! weight as determined by the [`DynamicMaxBlockWeight`] transaction extension. +//! +//! [`DynamicMaxBlockWeightHooks`] needs to be registered as a pre-inherent hook. It is used to +//! handle the weight consumption of `on_initialize` and change the block weight mode based on the +//! consumed weight. +//! +//! # Setup +//! +//! Setup the transaction extension: +#![doc = docify::embed!("src/block_weight/mock.rs", tx_extension_setup)] +//! +//! Setting up `MaximumBlockWeight`: +#![doc = docify::embed!("src/block_weight/mock.rs", max_block_weight_setup)] +//! +//! Registering of the `PreInherents` hook: +#![doc = docify::embed!("src/block_weight/mock.rs", pre_inherents_setup)] +//! # Weight per context +//! +//! Depending on the context, [`MaxParachainBlockWeight`] may returns a different max weight. The +//! max weight is only allowed to change in the first block of a core. Otherwise, all blocks need to +//! follow the target block weight determined based on the number of cores and the target block +//! rate. In the case of a first block, the following contexts may allow to access the full core +//! weight: +//! +//! - `on_initialize`: All logic that runs in this context up to the execution of `inherents` will +//! get access to the full core weight. +//! - `inherents`: Inherents also have access to the full core weight. +//! - `on_poll`: Only gets access to the target block weight. +//! - `transactions`: May get access to the full core weight, depends if they enable the access to +//! the full core weight based on the logic of [`DynamicMaxBlockWeight`]. +//! - `on_finalize`/`on_idle`: Only gets access to the target block weight. +//! +//! If any context that allows to use the full core weight, pushes the used block weight above the +//! target block weight, all other contexts will get access to the full core weight. + +use crate::{Config, PreviousCoreCount}; +use codec::{Decode, Encode}; +use core::marker::PhantomData; +use cumulus_primitives_core::CumulusDigestItem; +use frame_support::{ + weights::{constants::WEIGHT_REF_TIME_PER_SECOND, Weight}, + CloneNoBound, DebugNoBound, +}; +use frame_system::pallet_prelude::BlockNumberFor; +use polkadot_primitives::MAX_POV_SIZE; +use scale_info::TypeInfo; +use sp_core::Get; +use sp_runtime::Digest; + +#[cfg(test)] +mod mock; +pub mod pre_inherents_hook; +#[cfg(test)] +mod tests; +pub mod transaction_extension; + +pub use pre_inherents_hook::DynamicMaxBlockWeightHooks; +pub use transaction_extension::DynamicMaxBlockWeight; + +const LOG_TARGET: &str = "runtime::parachain-system::block-weight"; +/// Maximum ref time per core +const MAX_REF_TIME_PER_CORE_NS: u64 = 2 * WEIGHT_REF_TIME_PER_SECOND; +/// The available weight per core on the relay chain. +pub(crate) const FULL_CORE_WEIGHT: Weight = + Weight::from_parts(MAX_REF_TIME_PER_CORE_NS, MAX_POV_SIZE as u64); + +// Is set to `true` when we are currently inside of `pre_validate_extrinsic`. +environmental::environmental!(inside_pre_validate: bool); + +/// The current block weight mode. +/// +/// Based on this mode [`MaxParachainBlockWeight`] determines the current allowed block weight. +#[derive(DebugNoBound, Encode, Decode, CloneNoBound, TypeInfo, PartialEq)] +#[scale_info(skip_type_params(T))] +pub enum BlockWeightMode { + /// The block is allowed to use the weight of a full core. + FullCore { + /// The block in which this mode was set. Is used to determine if this is maybe stale mode + /// setting, e.g. when running `validate_block`. + context: BlockNumberFor, + }, + /// The current active transaction is allowed to use the weight of a full core. + PotentialFullCore { + /// The block in which this mode was set. Is used to determine if this is maybe stale mode + /// setting, e.g. when running `validate_block`. + context: BlockNumberFor, + /// The index of the first transaction. + /// + /// Stays `None` for all inherents until there is the first transaction. + first_transaction_index: Option, + /// The target weight that was used to determine that the extrinsic is above this limit. + target_weight: Weight, + }, + /// The block is only allowed to consume its fraction of the core. + /// + /// How much each block is allowed to consume, depends on the target number of blocks and the + /// available cores on the relay chain. + FractionOfCore { + /// The block in which this mode was set. Is used to determine if this is maybe stale mode + /// setting, e.g. when running `validate_block`. + context: BlockNumberFor, + /// The index of the first transaction. + /// + /// Stays `None` for all inherents until there is the first transaction. + first_transaction_index: Option, + }, +} + +impl BlockWeightMode { + /// Check if this mode is stale, aka was set in a previous block. + fn is_stale(&self) -> bool { + let context = self.context(); + + context < frame_system::Pallet::::block_number() + } + + /// Returns the context (block) in which this mode was set. + fn context(&self) -> BlockNumberFor { + match self { + Self::FullCore { context } | + Self::PotentialFullCore { context, .. } | + Self::FractionOfCore { context, .. } => *context, + } + } + + /// Create a new instance of `Self::FullCore`. + pub(crate) fn full_core() -> Self { + Self::FullCore { context: frame_system::Pallet::::block_number() } + } + + /// Create new instance of `Self::FractionOfCore`. + pub(crate) fn fraction_of_core(first_transaction_index: Option) -> Self { + Self::FractionOfCore { + context: frame_system::Pallet::::block_number(), + first_transaction_index, + } + } + + /// Create new instance of `Self::PotentialFullCore`. + pub(crate) fn potential_full_core( + first_transaction_index: Option, + target_weight: Weight, + ) -> Self { + Self::PotentialFullCore { + context: frame_system::Pallet::::block_number(), + first_transaction_index, + target_weight, + } + } +} + +/// Calculates the maximum block weight for a parachain. +/// +/// Based on the available cores and the number of desired blocks a block weight is calculated. +/// +/// The max block weight is partly dynamic and controlled via the [`DynamicMaxBlockWeight`] +/// transaction extension. The transaction extension is communicating the desired max block weight +/// using the [`BlockWeightMode`]. +pub struct MaxParachainBlockWeight(PhantomData<(Config, TargetBlockRate)>); + +impl> + MaxParachainBlockWeight +{ + /// Returns the target block weight for one block. + pub(crate) fn target_block_weight() -> Weight { + let digest = frame_system::Pallet::::digest(); + Self::target_block_weight_with_digest(&digest) + } + + /// Same as [`Self::target_block_weight`], but takes the `digests` directly. + fn target_block_weight_with_digest(digest: &Digest) -> Weight { + let number_of_cores = CumulusDigestItem::find_core_info(&digest).map_or_else( + || PreviousCoreCount::::get().map_or(1, |pc| pc.0), + |ci| ci.number_of_cores.0, + ) as u32; + + let target_blocks = TargetBlockRate::get(); + + // Ensure we have at least one core and valid target blocks + if number_of_cores == 0 || target_blocks == 0 { + return FULL_CORE_WEIGHT; + } + + // At maximum we want to allow `6s` of ref time, because we don't want to overload nodes + // that are running with standard hardware. These nodes need to be able to import all the + // blocks in `6s`. + let total_ref_time = (number_of_cores as u64) + .saturating_mul(MAX_REF_TIME_PER_CORE_NS) + .min(WEIGHT_REF_TIME_PER_SECOND * 6); + let ref_time_per_block = total_ref_time + .saturating_div(target_blocks as u64) + .min(MAX_REF_TIME_PER_CORE_NS); + + let total_pov_size = (number_of_cores as u64).saturating_mul(MAX_POV_SIZE as u64); + // Each block at max gets one core. + let proof_size_per_block = + total_pov_size.saturating_div(target_blocks as u64).min(MAX_POV_SIZE as u64); + + Weight::from_parts(ref_time_per_block, proof_size_per_block) + } +} + +impl> Get + for MaxParachainBlockWeight +{ + fn get() -> Weight { + let digest = frame_system::Pallet::::digest(); + let target_block_weight = Self::target_block_weight_with_digest(&digest); + + let maybe_full_core_weight = if is_first_block_in_core_with_digest(&digest).unwrap_or(false) + { + FULL_CORE_WEIGHT + } else { + target_block_weight + }; + + // Check if we are inside `pre_validate_extrinsic` of the transaction extension. + // + // When `pre_validate_extrinsic` calls this code, it is interested to know the + // `target_block_weight` which is then used to calculate the weight for each dispatch class. + // If `FullCore` mode is already enabled, the target weight is not important anymore. + let in_pre_validate = inside_pre_validate::with(|v| *v).unwrap_or(false); + + match crate::BlockWeightMode::::get().filter(|m| !m.is_stale()) { + // We allow the full core. + Some( + BlockWeightMode::::FullCore { .. } | + BlockWeightMode::::PotentialFullCore { .. }, + ) => FULL_CORE_WEIGHT, + // We are in `pre_validate`. + _ if in_pre_validate => target_block_weight, + // Only use the fraction of a core. + Some(BlockWeightMode::::FractionOfCore { first_transaction_index, .. }) => { + let is_phase_finalization = frame_system::Pallet::::execution_phase() + .map_or(false, |p| matches!(p, frame_system::Phase::Finalization)); + let inherents_applied = frame_system::Pallet::::inherents_applied(); + + if first_transaction_index.is_none() && !is_phase_finalization && !inherents_applied + { + // We are running in the context of inherents, here we allow the + // full core weight. + maybe_full_core_weight + } else { + // If we are finalizing the block (e.g. `on_idle` is running and + // `finalize_block`), running `on_poll` or nothing required more than the target + // block weight, we only allow the target block weight. + target_block_weight + } + }, + // We are in `on_initialize` or in an offchain context. + None => maybe_full_core_weight, + } + } +} + +/// Is this the first block in a core? +fn is_first_block_in_core() -> Option { + let digest = frame_system::Pallet::::digest(); + is_first_block_in_core_with_digest(&digest) +} + +/// Is this the first block in a core? (takes digest as parameter) +/// +/// Returns `None` if the [`CumulusDigestItem::BundleInfo`] digest is not set. +fn is_first_block_in_core_with_digest(digest: &Digest) -> Option { + CumulusDigestItem::find_bundle_info(digest).map(|bi| bi.index == 0) +} + +/// Is the `BlockWeight` already above the target block weight? +/// +/// Returns `None` if the [`CumulusDigestItem::BundleInfo`] digest is not set. +fn block_weight_over_target_block_weight>() -> bool { + let target_block_weight = MaxParachainBlockWeight::::target_block_weight(); + + frame_system::Pallet::::remaining_block_weight() + .consumed() + .any_gt(target_block_weight) +} diff --git a/cumulus/pallets/parachain-system/src/block_weight/pre_inherents_hook.rs b/cumulus/pallets/parachain-system/src/block_weight/pre_inherents_hook.rs new file mode 100644 index 0000000000000..4f9074a30eb32 --- /dev/null +++ b/cumulus/pallets/parachain-system/src/block_weight/pre_inherents_hook.rs @@ -0,0 +1,91 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Cumulus. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{ + block_weight_over_target_block_weight, is_first_block_in_core, BlockWeightMode, LOG_TARGET, +}; +use crate::block_weight::FULL_CORE_WEIGHT; +use cumulus_primitives_core::CumulusDigestItem; +use frame_support::{migrations::MultiStepMigrator, traits::PreInherents}; +use sp_core::Get; + +/// A pre-inherent hook that may increases max block weight after `on_initialize`. +/// +/// The hook is called before applying the first inherent. It checks the used block weight of +/// `on_initialize`. If the used block weight is above the target block weight, the hook will set +/// the [`CumulusDigestItem::UseFullCore`] digest. Regardless on if this is the first block in a +/// core or not. This is done to inform the node that this is the last block for the current core. +pub struct DynamicMaxBlockWeightHooks( + pub core::marker::PhantomData<(Config, TargetBlockRate)>, +); + +impl PreInherents for DynamicMaxBlockWeightHooks +where + Config: crate::Config, + TargetBlockRate: Get, +{ + fn pre_inherents() { + if !block_weight_over_target_block_weight::() { + let new_mode = if Config::MultiBlockMigrator::ongoing() { + log::debug!( + target: LOG_TARGET, + "Multi block migrations are still ongoing, allowing the full core.", + ); + + // Inform the node that this block uses the full core. + frame_system::Pallet::::deposit_log( + CumulusDigestItem::UseFullCore.to_digest_item(), + ); + + BlockWeightMode::::full_core() + } else { + BlockWeightMode::::fraction_of_core(None) + }; + + crate::BlockWeightMode::::put(new_mode); + + return + } + + let is_first_block_in_core = is_first_block_in_core::().unwrap_or(false); + + if !is_first_block_in_core { + log::error!( + target: LOG_TARGET, + "Inherent block logic took longer than the target block weight, THIS IS A BUG!!!", + ); + + // We are already above the allowed maximum and do not want to accept any more + // extrinsics. + frame_system::Pallet::::register_extra_weight_unchecked( + FULL_CORE_WEIGHT, + frame_support::dispatch::DispatchClass::Mandatory, + ); + } else { + log::debug!( + target: LOG_TARGET, + "Inherent block logic took longer than the target block weight, going to use the full core", + ); + } + + crate::BlockWeightMode::::put(BlockWeightMode::::full_core()); + + // Inform the node that this block uses the full core. + frame_system::Pallet::::deposit_log( + CumulusDigestItem::UseFullCore.to_digest_item(), + ); + } +} diff --git a/cumulus/pallets/parachain-system/src/block_weight/tests.rs b/cumulus/pallets/parachain-system/src/block_weight/tests.rs new file mode 100644 index 0000000000000..353dcdf53a338 --- /dev/null +++ b/cumulus/pallets/parachain-system/src/block_weight/tests.rs @@ -0,0 +1,1060 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Cumulus. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{mock::*, transaction_extension::DynamicMaxBlockWeight, *}; +use assert_matches::assert_matches; +use codec::Compact; +use cumulus_primitives_core::{ + BundleInfo, ClaimQueueOffset, CoreInfo, CoreSelector, CumulusDigestItem, +}; +use frame_support::{ + assert_ok, + dispatch::{DispatchClass, DispatchInfo, PostDispatchInfo}, + pallet_prelude::{InvalidTransaction, TransactionSource}, + traits::PreInherents, + weights::constants::WEIGHT_REF_TIME_PER_SECOND, +}; +use frame_system::{CheckWeight, RawOrigin as SystemOrigin}; +use polkadot_primitives::MAX_POV_SIZE; +use sp_core::ConstU32; +use sp_runtime::{ + traits::{DispatchTransaction, Header, TransactionExtension}, + Digest, +}; + +type TxExtension = DynamicMaxBlockWeight, ConstU32<4>>; +type TxExtensionOnlyOperational = + DynamicMaxBlockWeight, ConstU32<4>, 10, false>; +type MaximumBlockWeight = MaxParachainBlockWeight>; + +#[test] +fn test_single_core_single_block() { + TestExtBuilder::new().number_of_cores(1).build().execute_with(|| { + let weight = MaxParachainBlockWeight::>::get(); + + assert_eq!(weight.ref_time(), 2 * WEIGHT_REF_TIME_PER_SECOND); + assert_eq!(weight.proof_size(), MAX_POV_SIZE as u64); + }); +} + +#[test] +fn test_single_core_multiple_blocks() { + TestExtBuilder::new().number_of_cores(1).build().execute_with(|| { + let weight = MaxParachainBlockWeight::>::get(); + + // With 1 core and 4 target blocks, should get 0.5s ref time and 1/4 PoV size per block + assert_eq!(weight.ref_time(), 2 * WEIGHT_REF_TIME_PER_SECOND / 4); + assert_eq!(weight.proof_size(), (1 * MAX_POV_SIZE as u64) / 4); + }); +} + +#[test] +fn test_multiple_cores_single_block() { + TestExtBuilder::new().number_of_cores(3).build().execute_with(|| { + let weight = MaxParachainBlockWeight::>::get(); + + // With 3 cores and 1 target blocks, should get 2s ref time and 1 PoV size + assert_eq!(weight.ref_time(), 2 * WEIGHT_REF_TIME_PER_SECOND); + assert_eq!(weight.proof_size(), MAX_POV_SIZE as u64); + }); +} + +#[test] +fn test_multiple_cores_multiple_blocks() { + TestExtBuilder::new().number_of_cores(2).build().execute_with(|| { + let weight = MaxParachainBlockWeight::>::get(); + + // With 2 cores and 4 target blocks, should get 1s ref time and 2x PoV size / 4 per + // block + assert_eq!(weight.ref_time(), 2 * 2 * WEIGHT_REF_TIME_PER_SECOND / 4); + assert_eq!(weight.proof_size(), (2 * MAX_POV_SIZE as u64) / 4); + }); +} + +#[test] +fn test_no_core_info() { + TestExtBuilder::new().build().execute_with(|| { + let weight = MaxParachainBlockWeight::>::get(); + + // Without core info, it takes the `PreviousCoreCount` into account. + assert_eq!(weight.ref_time(), 2 * WEIGHT_REF_TIME_PER_SECOND / 4); + assert_eq!(weight.proof_size(), MAX_POV_SIZE as u64 / 4); + }); +} + +#[test] +fn test_zero_cores() { + TestExtBuilder::new().number_of_cores(0).build().execute_with(|| { + let weight = MaxParachainBlockWeight::>::get(); + + // With 0 cores, should return conservative default + assert_eq!(weight.ref_time(), 2 * WEIGHT_REF_TIME_PER_SECOND); + assert_eq!(weight.proof_size(), MAX_POV_SIZE as u64); + }); +} + +#[test] +fn test_zero_target_blocks() { + TestExtBuilder::new().number_of_cores(2).build().execute_with(|| { + let weight = MaxParachainBlockWeight::>::get(); + assert_eq!(weight.ref_time(), 2 * WEIGHT_REF_TIME_PER_SECOND); + assert_eq!(weight.proof_size(), MAX_POV_SIZE as u64); + }); +} + +#[test] +fn test_target_block_weight_calculation() { + TestExtBuilder::new().number_of_cores(4).build().execute_with(|| { + // Test target_block_weight function directly + // Both calls return the same since ConstU32<4> is fixed at compile time + let weight = MaxParachainBlockWeight::>::target_block_weight(); + + assert_eq!(weight.ref_time(), 3 * 2 * WEIGHT_REF_TIME_PER_SECOND / 4); + assert_eq!(weight.proof_size(), MAX_POV_SIZE as u64); + }); +} + +#[test] +fn test_max_ref_time_per_core_cap() { + TestExtBuilder::new().number_of_cores(8).build().execute_with(|| { + // With 8 cores and 4 target blocks, ref time per block should be capped at 2s per core + let weight = MaxParachainBlockWeight::>::get(); + + // 8 cores * 2s = 16s total, divided by 4 blocks = 4s, but capped at 6s for all blocks in + // total + assert_eq!(weight.ref_time(), 2 * WEIGHT_REF_TIME_PER_SECOND * 3 / 4); + assert_eq!(weight.proof_size(), MAX_POV_SIZE as u64); + }); +} + +#[test] +fn test_target_block_weight_with_digest_edge_cases() { + TestExtBuilder::new().build().execute_with(|| { + // Test with empty digest + let empty_digest = Digest::default(); + let weight = + MaxParachainBlockWeight::>::target_block_weight_with_digest( + &empty_digest, + ); + assert_eq!(weight, FULL_CORE_WEIGHT / 4); + + // Test with digest containing core info + let core_info = CoreInfo { + selector: CoreSelector(0), + claim_queue_offset: ClaimQueueOffset(0), + number_of_cores: Compact(2u16), + }; + + let digest = Digest { logs: vec![CumulusDigestItem::CoreInfo(core_info).to_digest_item()] }; + + // With 2 cores and 4 target blocks: (2 cores * 2s) / 4 blocks = 1s + let weight = + MaxParachainBlockWeight::>::target_block_weight_with_digest( + &digest, + ); + assert_eq!(weight.ref_time(), 2 * 2 * WEIGHT_REF_TIME_PER_SECOND / 4); + assert_eq!(weight.proof_size(), (2 * MAX_POV_SIZE as u64) / 4); + }); +} + +#[test] +fn test_is_first_block_in_core_functions() { + TestExtBuilder::new().number_of_cores(1).build().execute_with(|| { + let empty_digest = Digest::default(); + assert!(super::is_first_block_in_core_with_digest(&empty_digest).is_none()); + + // Test with bundle info index = 0 - should return true + let bundle_info_first = BundleInfo { index: 0, maybe_last: false }; + let digest_item_first = CumulusDigestItem::BundleInfo(bundle_info_first).to_digest_item(); + let mut digest_first = Digest::default(); + digest_first.push(digest_item_first); + assert!(super::is_first_block_in_core_with_digest(&digest_first).unwrap()); + + // Test with bundle info index > 0 - should return false + let bundle_info_not_first = BundleInfo { index: 5, maybe_last: true }; + let digest_item_not_first = + CumulusDigestItem::BundleInfo(bundle_info_not_first).to_digest_item(); + let mut digest_not_first = Digest::default(); + digest_not_first.push(digest_item_not_first); + assert!(!super::is_first_block_in_core_with_digest(&digest_not_first).unwrap()); + }); +} + +#[test] +fn tx_extension_sets_fraction_of_core_mode() { + use frame_support::dispatch::{DispatchClass, DispatchInfo}; + + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(true) + .build() + .execute_with(|| { + initialize_block_finished(); + + // Create a small transaction + let small_weight = Weight::from_parts(100_000, 1024); + let info = DispatchInfo { + call_weight: small_weight, + class: DispatchClass::Normal, + pays_fee: frame_support::dispatch::Pays::Yes, + ..Default::default() + }; + + assert_ok!(TxExtension::validate_and_prepare( + TxExtension::new(Default::default()), + SystemOrigin::Signed(0).into(), + &CALL, + &info, + 100, + 0, + )); + + assert_eq!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::fraction_of_core(Some(0))) + ); + }); +} + +#[test] +fn tx_extension_large_tx_enables_full_core_usage() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(true) + .build() + .execute_with(|| { + initialize_block_finished(); + + // Create a transaction larger than target weight + let target_weight = MaximumBlockWeight::target_block_weight(); + let large_weight = target_weight + .saturating_add(Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 1024 * 1024)); + + let info = DispatchInfo { + call_weight: large_weight, + class: DispatchClass::Normal, + ..Default::default() + }; + + System::set_extrinsic_index(1); + + assert_ok!(TxExtension::validate_and_prepare( + TxExtension::new(Default::default()), + SystemOrigin::Signed(0).into(), + &CALL, + &info, + 100, + 0, + )); + + assert_matches!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::PotentialFullCore { first_transaction_index: Some(1), .. }) + ); + + let mut post_info = + PostDispatchInfo { actual_weight: None, pays_fee: Default::default() }; + + assert_ok!(TxExtension::post_dispatch((), &info, &mut post_info, 0, &Ok(()))); + + assert_eq!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::full_core()) + ); + assert!(has_use_full_core_digest()); + assert_eq!(MaximumBlockWeight::get().ref_time(), 2 * WEIGHT_REF_TIME_PER_SECOND); + }); +} + +#[test] +fn tx_extension_only_allows_large_operational_tx_to_enable_full_core_usage() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(true) + .build() + .execute_with(|| { + initialize_block_finished(); + + // Create a transaction larger than target weight + let target_weight = MaximumBlockWeight::target_block_weight(); + let large_weight = target_weight + .saturating_add(Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 1024 * 1024)); + + let mut info = DispatchInfo { + call_weight: large_weight, + class: DispatchClass::Normal, + ..Default::default() + }; + + // As `Normal` transaction this should be rejected. + assert_eq!( + TxExtensionOnlyOperational::validate_and_prepare( + TxExtensionOnlyOperational::new(Default::default()), + SystemOrigin::Signed(0).into(), + &CALL, + &info, + 100, + 0, + ) + .unwrap_err(), + InvalidTransaction::ExhaustsResources.into() + ); + + assert_matches!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::FractionOfCore { first_transaction_index: None, .. }) + ); + + info.class = DispatchClass::Operational; + + // As `Operational` transaction this is accepted. + assert_ok!(TxExtensionOnlyOperational::validate_and_prepare( + TxExtensionOnlyOperational::new(Default::default()), + SystemOrigin::Signed(0).into(), + &CALL, + &info, + 100, + 0, + )); + + assert_matches!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::PotentialFullCore { first_transaction_index: Some(0), .. }) + ); + + let mut post_info = + PostDispatchInfo { actual_weight: None, pays_fee: Default::default() }; + + assert_ok!(TxExtension::post_dispatch((), &info, &mut post_info, 0, &Ok(()))); + + assert_eq!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::full_core()) + ); + assert!(has_use_full_core_digest()); + assert_eq!(MaximumBlockWeight::get().ref_time(), 2 * WEIGHT_REF_TIME_PER_SECOND); + }); +} + +#[test] +fn tx_extension_large_tx_with_refund_goes_back_to_fractional() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(true) + .build() + .execute_with(|| { + initialize_block_finished(); + + System::set_extrinsic_index(1); + + // Create a transaction larger than target weight + let target_weight = MaximumBlockWeight::target_block_weight(); + let large_weight = target_weight + .saturating_add(Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 1024 * 1024)); + + let info = DispatchInfo { + call_weight: large_weight, + class: DispatchClass::Normal, + ..Default::default() + }; + + assert_ok!(TxExtension::validate_and_prepare( + TxExtension::new(Default::default()), + SystemOrigin::Signed(0).into(), + &CALL, + &info, + 100, + 0, + )); + + assert_matches!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::PotentialFullCore { first_transaction_index: Some(1), .. }) + ); + + let mut post_info = PostDispatchInfo { + actual_weight: Some(Weight::from_parts(5000, 5000)), + pays_fee: Default::default(), + }; + + assert_ok!(TxExtension::post_dispatch((), &info, &mut post_info, 0, &Ok(()))); + + assert_matches!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::FractionOfCore { .. }) + ); + assert!(!has_use_full_core_digest()); + assert_eq!(MaximumBlockWeight::get(), target_weight); + }); +} + +#[test] +fn tx_extension_large_tx_is_rejected_on_non_first_block() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(false) + .build() + .execute_with(|| { + initialize_block_finished(); + + // Create a transaction larger than target weight + let target_weight = MaximumBlockWeight::target_block_weight(); + let large_weight = target_weight + .saturating_add(Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 1024 * 1024)); + + let info = DispatchInfo { + call_weight: large_weight, + class: DispatchClass::Normal, + ..Default::default() + }; + + assert_eq!( + TxExtension::validate_and_prepare( + TxExtension::new(Default::default()), + SystemOrigin::Signed(0).into(), + &CALL, + &info, + 100, + 0, + ) + .unwrap_err(), + InvalidTransaction::ExhaustsResources.into() + ); + + // Should stay in FractionOfCore mode (not PotentialFullCore) since not first block + assert_eq!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::fraction_of_core(None)) + ); + assert!(!has_use_full_core_digest()); + assert_eq!(MaximumBlockWeight::get(), target_weight); + }); +} + +#[test] +fn tx_extension_post_dispatch_to_full_core_because_of_manual_weight() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(false) + .build() + .execute_with(|| { + initialize_block_finished(); + + let target_weight = + MaxParachainBlockWeight::>::target_block_weight(); + + // Transaction announces small weight + let small_weight = Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND / 10, 1024); + let info = DispatchInfo { call_weight: small_weight, ..Default::default() }; + + assert_ok!(TxExtension::validate_and_prepare( + TxExtension::new(Default::default()), + SystemOrigin::Signed(0).into(), + &CALL, + &info, + 100, + 0, + )); + + assert_matches!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::FractionOfCore { first_transaction_index: Some(0), .. }) + ); + + // But actually uses much more weight (bug in weight annotation) + let large_weight = target_weight + .saturating_add(Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 1024 * 1024)); + register_weight(large_weight, DispatchClass::Normal); + + let mut post_info = + PostDispatchInfo { actual_weight: None, pays_fee: Default::default() }; + assert_ok!(TxExtension::post_dispatch((), &info, &mut post_info, 0, &Ok(()))); + + // Should transition to FullCore due to exceeding limit + assert_matches!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::FullCore { .. }) + ); + + assert!(has_use_full_core_digest()); + }); +} + +#[test] +fn tx_extension_large_tx_after_limit_is_rejected() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(true) + .build() + .execute_with(|| { + initialize_block_finished(); + + // Set some index above the limit. + System::set_extrinsic_index(20); + + // Create a transaction larger than target weight + let target_weight = MaximumBlockWeight::target_block_weight(); + let large_weight = target_weight + .saturating_add(Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 1024 * 1024)); + + let info = DispatchInfo { call_weight: large_weight, ..Default::default() }; + + assert_eq!( + TxExtension::validate_and_prepare( + TxExtension::new(Default::default()), + SystemOrigin::Signed(0).into(), + &CALL, + &info, + 100, + 0, + ) + .unwrap_err(), + InvalidTransaction::ExhaustsResources.into() + ); + + assert_eq!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::fraction_of_core(None)) + ); + assert!(!has_use_full_core_digest()); + }); +} + +#[test] +fn tx_extension_large_weight_before_first_tx() { + for first_block_in_core in [true, false] { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(first_block_in_core) + .build() + .execute_with(|| { + initialize_block_finished(); + + let target_weight = MaximumBlockWeight::target_block_weight(); + let large_weight = target_weight + .saturating_add(Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 1024 * 1024)); + + register_weight(large_weight, DispatchClass::Normal); + + let small_weight = Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND / 10, 1024); + let info = DispatchInfo { call_weight: small_weight, ..Default::default() }; + + let res = TxExtension::validate_and_prepare( + TxExtension::new(Default::default()), + SystemOrigin::Signed(0).into(), + &CALL, + &info, + 100, + 0, + ); + + if first_block_in_core { + assert!(res.is_ok()) + } else { + assert_eq!(res.unwrap_err(), InvalidTransaction::ExhaustsResources.into()); + } + + assert_matches!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::FullCore { .. }) + ); + + assert!(has_use_full_core_digest()); + assert_eq!(MaximumBlockWeight::get().ref_time(), 2 * WEIGHT_REF_TIME_PER_SECOND); + + if !first_block_in_core { + // Should have registered FULL_CORE_WEIGHT to prevent more transactions + let final_remaining = frame_system::Pallet::::remaining_block_weight(); + assert!(final_remaining.consumed().all_gte(FULL_CORE_WEIGHT)); + } + }); + } +} + +#[test] +fn pre_inherents_hook_first_block_over_limit() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(true) + .build() + .execute_with(|| { + // Simulate on_initialize consuming more than target weight + let target_weight = MaximumBlockWeight::target_block_weight(); + let excessive_weight = target_weight + .saturating_add(Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 1024 * 1024)); + + register_weight(excessive_weight, DispatchClass::Mandatory); + + // Call pre_inherents hook + DynamicMaxBlockWeightHooks::>::pre_inherents(); + + assert_matches!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::FullCore { .. }) + ); + + // Should have UseFullCore digest + assert!(has_use_full_core_digest()); + }); +} + +#[test] +fn pre_inherents_hook_non_first_block_over_limit() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(false) + .build() + .execute_with(|| { + // Simulate on_initialize consuming more than target weight + let target_weight = MaximumBlockWeight::target_block_weight(); + let excessive_weight = target_weight + .saturating_add(Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 1024 * 1024)); + + register_weight(excessive_weight, DispatchClass::Mandatory); + + // Call pre_inherents hook + DynamicMaxBlockWeightHooks::>::pre_inherents(); + + assert_matches!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::FullCore { .. }) + ); + + assert!(has_use_full_core_digest()); + + // Should have registered FULL_CORE_WEIGHT to prevent more transactions + let final_remaining = frame_system::Pallet::::remaining_block_weight(); + assert!(final_remaining.consumed().all_gte(FULL_CORE_WEIGHT)); + }); +} + +#[test] +fn pre_inherents_hook_under_limit_no_change() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(true) + .build() + .execute_with(|| { + // Simulate on_initialize consuming less than target weight + let target_weight = MaximumBlockWeight::target_block_weight(); + let small_weight = + Weight::from_parts(target_weight.ref_time() / 2, target_weight.proof_size() / 2); + + register_weight(small_weight, DispatchClass::Mandatory); + + // Call pre_inherents hook + DynamicMaxBlockWeightHooks::>::pre_inherents(); + + assert_matches!( + crate::BlockWeightMode::::get(), + Some(BlockWeightMode::FractionOfCore { first_transaction_index: None, .. }) + ); + + // Should NOT have UseFullCore digest + assert!(!has_use_full_core_digest()); + }); +} + +#[test] +fn max_weight_without_bundle_info() { + TestExtBuilder::new().number_of_cores(2).build().execute_with(|| { + // Without bundle info, cannot determine if first block + // Should still work but max weight determination will be conservative + + frame_system::Pallet::::note_finished_initialize(); + + let max_weight = MaximumBlockWeight::get(); + + // With 2 cores and 12 target blocks + let expected_weight = Weight::from_parts( + 2 * 2 * WEIGHT_REF_TIME_PER_SECOND / TARGET_BLOCK_RATE as u64, + 2 * MAX_POV_SIZE as u64 / TARGET_BLOCK_RATE as u64, + ); + + assert_eq!(max_weight, expected_weight); + }); +} + +#[test] +fn ref_time_and_pov_size_cap() { + TestExtBuilder::new().number_of_cores(10).build().execute_with(|| { + frame_system::Pallet::::note_finished_initialize(); + + let max_weight = MaxParachainBlockWeight::>::get(); + + // At most one core will always only be able to use the resources of one core. + assert_eq!(max_weight.ref_time(), 2 * WEIGHT_REF_TIME_PER_SECOND); + assert_eq!(max_weight.proof_size(), MAX_POV_SIZE as u64); + + let max_weight = MaxParachainBlockWeight::>::get(); + + // Each blocks get its own core (can use the max pov size), but ref time of all blocks + // together is in max `6s` + assert_eq!(max_weight.ref_time(), 6 * WEIGHT_REF_TIME_PER_SECOND / 4); + assert_eq!(max_weight.proof_size(), MAX_POV_SIZE as u64); + }); +} + +#[test] +fn executive_validate_block_handles_normal_transactions() { + for signed in [true, false] { + TestExtBuilder::new().previous_core_count(3).build().execute_with(|| { + let call = RuntimeCall::TestPallet(test_pallet::Call::heavy_call_normal {}); + + let xt = if signed { + Extrinsic::new_signed(call, 1u64.into(), 1u64.into(), Default::default()) + } else { + Extrinsic::new_bare(call) + }; + + assert_ok!(Executive::validate_transaction( + TransactionSource::External, + xt.clone(), + Default::default() + )); + }); + + TestExtBuilder::new().previous_core_count(3).build().execute_with(|| { + let call = + RuntimeCallOnlyOperational::TestPallet(test_pallet::Call::heavy_call_normal {}); + + let xt = if signed { + ExtrinsicOnlyOperational::new_signed( + call, + 1u64.into(), + 1u64.into(), + Default::default(), + ) + } else { + ExtrinsicOnlyOperational::new_bare(call) + }; + + assert_eq!( + ExecutiveOnlyOperational::validate_transaction( + TransactionSource::External, + xt, + Default::default() + ) + .unwrap_err(), + InvalidTransaction::ExhaustsResources.into() + ); + }); + } +} + +#[test] +fn executive_validate_block_handles_operational_transactions() { + for signed in [true, false] { + TestExtBuilder::new().previous_core_count(3).build().execute_with(|| { + let call = RuntimeCall::TestPallet(test_pallet::Call::heavy_call_operational {}); + + let xt = if signed { + Extrinsic::new_signed(call, 1u64.into(), 1u64.into(), Default::default()) + } else { + Extrinsic::new_bare(call) + }; + + assert_ok!(Executive::validate_transaction( + TransactionSource::External, + xt.clone(), + Default::default() + )); + }); + + TestExtBuilder::new().previous_core_count(3).build().execute_with(|| { + let call = RuntimeCallOnlyOperational::TestPallet( + test_pallet::Call::heavy_call_operational {}, + ); + + let xt = if signed { + ExtrinsicOnlyOperational::new_signed( + call, + 1u64.into(), + 1u64.into(), + Default::default(), + ) + } else { + ExtrinsicOnlyOperational::new_bare(call) + }; + + assert!(ExecutiveOnlyOperational::validate_transaction( + TransactionSource::External, + xt, + Default::default() + ) + .is_ok()); + }); + } +} + +#[test] +fn executive_with_operational_only_applies_big_inherent() { + TestExtBuilder::new() + .number_of_cores(1) + .first_block_in_core(true) + .build() + .execute_with(|| { + Executive::initialize_block(&Header::new( + 1, + Default::default(), + Default::default(), + Default::default(), + System::digest(), + )); + + let call = + RuntimeCallOnlyOperational::TestPallet(test_pallet::Call::heavy_call_mandatory {}); + + let xt = ExtrinsicOnlyOperational::new_bare(call); + + ExecutiveOnlyOperational::apply_extrinsic(xt).unwrap().unwrap(); + }); +} + +#[test] +fn block_weight_mode_from_previous_block_is_ignored_in_validate_block() { + TestExtBuilder::new() + .number_of_cores(4) + .first_block_in_core(true) + .build() + .execute_with(|| { + let call = RuntimeCallOnlyOperational::TestPallet( + test_pallet::Call::heavy_call_operational {}, + ); + + let xt = ExtrinsicOnlyOperational::new_signed( + call, + 1u64.into(), + 1u64.into(), + Default::default(), + ); + + ExecutiveOnlyOperational::initialize_block(&Header::new( + 1, + Default::default(), + Default::default(), + Default::default(), + System::digest(), + )); + + assert_ok!(ExecutiveOnlyOperational::apply_extrinsic(xt)); + + fake_set_validation_data(); + + ExecutiveOnlyOperational::finalize_block(); + + assert_eq!( + crate::BlockWeightMode::::get().unwrap(), + BlockWeightMode::full_core() + ); + + let call = + RuntimeCallOnlyOperational::TestPallet(test_pallet::Call::heavy_call_normal {}); + + let xt = ExtrinsicOnlyOperational::new_signed( + call, + 1u64.into(), + 1u64.into(), + Default::default(), + ); + + assert_eq!( + ExecutiveOnlyOperational::validate_transaction( + TransactionSource::External, + xt, + Default::default() + ) + .unwrap_err(), + InvalidTransaction::ExhaustsResources.into() + ); + }); +} + +#[test] +fn ongoing_mbm_requests_full_core() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(true) + .build() + .execute_with(|| { + MbmOngoing::set(true); + ExecutiveOnlyOperational::initialize_block(&Header::new( + 1, + Default::default(), + Default::default(), + Default::default(), + System::digest(), + )); + + assert_eq!( + FULL_CORE_WEIGHT, + ::BlockWeights::get().max_block + ); + + fake_set_validation_data(); + + ExecutiveOnlyOperational::finalize_block(); + + assert!(has_use_full_core_digest()); + MbmOngoing::set(false); + }); +} + +#[test] +fn ignores_previous_block_weight_in_on_initialize() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(true) + .build() + .execute_with(|| { + crate::BlockWeightMode::::put( + BlockWeightMode::fraction_of_core(None), + ); + + // Start a new block + System::set_block_number(1); + + assert_eq!(MaximumBlockWeight::get(), FULL_CORE_WEIGHT); + }); +} + +#[test] +fn full_core_weight_in_inherent_context() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(true) + .build() + .execute_with(|| { + Executive::initialize_block(&Header::new( + 1, + Default::default(), + Default::default(), + Default::default(), + System::digest(), + )); + + assert!(!frame_system::Pallet::::inherents_applied()); + + assert_eq!(MaximumBlockWeight::get(), FULL_CORE_WEIGHT); + }); +} + +#[test] +fn executive_validate_transaction_respects_dispatch_class_max_block_size() { + // Create some weight which is slightly above the allowed dispatch class max size. + let call_weight = TestExtBuilder::new().previous_core_count(4).build().execute_with(|| { + MaximumBlockWeight::target_block_weight() * NORMAL_DISPATCH_RATIO + Weight::from_parts(1, 1) + }); + + for signed in [true, false] { + TestExtBuilder::new().previous_core_count(4).build().execute_with(|| { + assert!(::BlockWeights::get() + .get(DispatchClass::Normal) + .max_total + .unwrap() + .all_lt(call_weight)); + assert!(MaximumBlockWeight::target_block_weight().all_gt(call_weight)); + + let call = + RuntimeCall::TestPallet(test_pallet::Call::use_weight { weight: call_weight }); + + let xt = if signed { + Extrinsic::new_signed(call, 1u64.into(), 1u64.into(), Default::default()) + } else { + Extrinsic::new_bare(call) + }; + + assert_ok!(Executive::validate_transaction( + TransactionSource::External, + xt.clone(), + Default::default() + )); + }); + + TestExtBuilder::new().previous_core_count(4).build().execute_with(|| { + let call = RuntimeCallOnlyOperational::TestPallet(test_pallet::Call::use_weight { + weight: call_weight, + }); + + let xt = if signed { + ExtrinsicOnlyOperational::new_signed( + call, + 1u64.into(), + 1u64.into(), + Default::default(), + ) + } else { + ExtrinsicOnlyOperational::new_bare(call) + }; + + assert_eq!( + ExecutiveOnlyOperational::validate_transaction( + TransactionSource::External, + xt, + Default::default() + ) + .unwrap_err(), + InvalidTransaction::ExhaustsResources.into() + ); + }); + } +} + +#[test] +fn on_idle_uses_correct_weight() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(true) + .build() + .execute_with(|| { + Executive::initialize_block(&Header::new( + 1, + Default::default(), + Default::default(), + Default::default(), + System::digest(), + )); + + fake_set_validation_data(); + + OnIdleMaxLeftWeight::set(Some(MaximumBlockWeight::target_block_weight())); + + Executive::finalize_block(); + }); +} + +#[test] +fn on_poll_uses_correct_weight() { + TestExtBuilder::new() + .number_of_cores(2) + .first_block_in_core(true) + .build() + .execute_with(|| { + Executive::initialize_block(&Header::new( + 1, + Default::default(), + Default::default(), + Default::default(), + System::digest(), + )); + + fake_set_validation_data(); + + OnPollMaxLeftWeight::set(Some(MaximumBlockWeight::target_block_weight())); + + Executive::finalize_block(); + }); +} diff --git a/cumulus/pallets/parachain-system/src/block_weight/transaction_extension.rs b/cumulus/pallets/parachain-system/src/block_weight/transaction_extension.rs new file mode 100644 index 0000000000000..017468130caf5 --- /dev/null +++ b/cumulus/pallets/parachain-system/src/block_weight/transaction_extension.rs @@ -0,0 +1,513 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Cumulus. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{ + block_weight_over_target_block_weight, inside_pre_validate, is_first_block_in_core_with_digest, + BlockWeightMode, MaxParachainBlockWeight, FULL_CORE_WEIGHT, LOG_TARGET, +}; +use crate::WeightInfo; +use alloc::vec::Vec; +use codec::{Decode, DecodeWithMemTracking, Encode}; +use cumulus_primitives_core::CumulusDigestItem; +use frame_support::{ + dispatch::{DispatchClass, DispatchInfo, PostDispatchInfo}, + pallet_prelude::{ + InvalidTransaction, TransactionSource, TransactionValidityError, ValidTransaction, + }, + weights::Weight, +}; +use scale_info::TypeInfo; +use sp_core::Get; +use sp_runtime::{ + traits::{DispatchInfoOf, Dispatchable, Implication, PostDispatchInfoOf, TransactionExtension}, + DispatchResult, +}; + +/// Transaction extension that dynamically changes the max block weight. +/// +/// With block bundling, parachains are running with block weights that may not allow certain +/// transactions to be applied, e.g. a runtime upgrade. To ensure that these transactions can still +/// be applied, this transaction extension can change the max block weight as required. There are +/// multiple requirements for it to change the block weight: +/// +/// 1. Only the first block of a core is allowed to change its block weight. +/// +/// 2. Any `inherent` or any transaction up to `MAX_TRANSACTION_TO_CONSIDER` requires more block +/// weight than the target block weight. Target block weight is the max weight for the respective +/// extrinsic class. +/// +/// Because the node is tracking the wall clock time while building a block to abort block +/// production if it takes too long, we do not allow any block to change the block weight. The node +/// knows that the first block of a core may runs longer. So, the node allows this block to take up +/// to `2s` of wall clock time. `2s` is the time each `PoV` gets on the relay chain for its +/// validation or in other words the maximum core execution time. The extension sets the +/// [`CumulusDigestItem::UseFullCore`] digest when the block should occupy the entire core. +/// +/// Before dispatching an extrinsic the extension will check the requirements and set the +/// appropriate [`BlockWeightMode`]. After the extrinsic has finished, the checks from before +/// dispatching the extrinsic are repeated with the post dispatch weights. The [`BlockWeightMode`] +/// is changed properly. +/// +/// # Note +/// +/// The extension requires that any of the inner extensions sets the +/// [`BlockWeight`](frame_system::BlockWeight). Otherwise the weight tracking is not working +/// properly. Normally this is done by [`CheckWeight`](frame_system::CheckWeight). +/// +/// # Generic parameters +/// +/// - `Config`: The [`Config`](crate::Config) trait of this pallet. +/// +/// - `Inner`: The inner transaction extensions aka the other transaction extensions to be used by +/// the runtime. +/// +/// - `TargetBlockRate`: The target block rate the parachain should be running with. Or in other +/// words, the number of blocks the parachain should produce in `6s`(relay chain slot duration). +/// +/// - `MAX_TRANSACTION`: The maximum number of transactions to consider before giving up to change +/// the max block weight. +/// +/// - `ALLOW_NORMAL`: Should transactions with a dispatch class `Normal` be allowed to change the +/// max block weight? +#[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo)] +#[derive_where::derive_where(Clone, Eq, PartialEq, Default; Inner)] +#[scale_info(skip_type_params(Config, TargetBlockRate))] +pub struct DynamicMaxBlockWeight< + Config, + Inner, + TargetBlockRate, + const MAX_TRANSACTION_TO_CONSIDER: u32 = 10, + const ALLOW_NORMAL: bool = true, +>(pub Inner, core::marker::PhantomData<(Config, TargetBlockRate)>); + +impl + DynamicMaxBlockWeight +{ + /// Create a new [`DynamicMaxBlockWeight`] instance. + pub fn new(s: S) -> Self { + Self(s, Default::default()) + } +} + +impl< + Config, + Inner, + TargetBlockRate, + const MAX_TRANSACTION_TO_CONSIDER: u32, + const ALLOW_NORMAL: bool, + > DynamicMaxBlockWeight +where + Config: crate::Config, + TargetBlockRate: Get, +{ + /// Should be executed before `validate` is called for any inner extension. + fn pre_validate_extrinsic( + info: &DispatchInfo, + len: usize, + ) -> Result<(), TransactionValidityError> { + let is_not_inherent = frame_system::Pallet::::inherents_applied(); + let extrinsic_index = frame_system::Pallet::::extrinsic_index().unwrap_or_default(); + let transaction_index = is_not_inherent.then(|| extrinsic_index); + + crate::BlockWeightMode::::mutate(|mode| { + let current_mode = mode.get_or_insert_with(|| BlockWeightMode::::fraction_of_core(transaction_index)); + + // If the mode is stale (from previous block), we reset it. + // + // This happens for example when running in an offchain context. + if current_mode.is_stale() { + *current_mode = BlockWeightMode::fraction_of_core(transaction_index); + } + + log::trace!( + target: LOG_TARGET, + "About to pre-validate an extrinsic. current_mode={current_mode:?}, transaction_index={transaction_index:?}" + ); + + let is_potential = + matches!(current_mode, &mut BlockWeightMode::PotentialFullCore { .. }); + + match current_mode { + // We are already allowing the full core, not that much more to do here. + BlockWeightMode::::FullCore { .. } => {}, + BlockWeightMode::::PotentialFullCore { first_transaction_index, .. } | + BlockWeightMode::::FractionOfCore { first_transaction_index, .. } => { + debug_assert!( + !is_potential, + "`PotentialFullCore` should resolve to `FullCore` or `FractionOfCore` after applying a transaction.", + ); + + let digest = frame_system::Pallet::::digest(); + let block_weight_over_limit = extrinsic_index == 0 + && block_weight_over_target_block_weight::(); + + // If `BlockWeights` is configured correctly, it will internally call `MaxParachainBlockWeight::get()` + // and by setting this variable to `true`, we tell it the context. This is important as we want to get + // the `target_block_weight` and not the full core weight. Otherwise, we will here get a too huge weight + // and do not set the `PotentialFullCore` weight, leading to `CheckWeight` rejecting the extrinsic. + // + // All of this is only important for extrinsics that will enable the `PotentialFullCore` mode. + let block_weights = inside_pre_validate::using(&mut true, || Config::BlockWeights::get()); + let target_weight = block_weights + .get(info.class) + .max_total + .unwrap_or_else(|| + MaxParachainBlockWeight::::target_block_weight_with_digest(&digest) + .saturating_sub(block_weights.base_block) + ); + + // Protection against a misconfiguration as this should be detected by the pre-inherent hook. + if block_weight_over_limit { + *mode = Some(BlockWeightMode::::full_core()); + + // Inform the node that this block uses the full core. + frame_system::Pallet::::deposit_log( + CumulusDigestItem::UseFullCore.to_digest_item(), + ); + + if !is_first_block_in_core_with_digest(&digest).unwrap_or(false) { + // We are already above the allowed maximum and do not want to accept any more + // extrinsics. + frame_system::Pallet::::register_extra_weight_unchecked( + FULL_CORE_WEIGHT, + DispatchClass::Mandatory, + ); + } + + log::error!( + target: LOG_TARGET, + "Inherent block logic took longer than the target block weight, \ + `DynamicMaxBlockWeightHooks` not registered as `PreInherents` hook!", + ); + } else if info + .total_weight() + // The extrinsic lengths counts towards the POV size + .saturating_add(Weight::from_parts(0, len as u64)) + .any_gt(target_weight) + { + // When `ALLOW_NORMAL` is `true`, we want to allow all classes of transactions. Inherents are always allowed. + let class_allowed = if ALLOW_NORMAL { true } else { info.class == DispatchClass::Operational } + || info.class == DispatchClass::Mandatory; + + // If the `BundleInfo` digest is not set (function returns `None`), it means we are in some offchain + // call like `validate_block`. In this case we assume this is the first block, otherwise these big + // transactions will never be able to enter the tx pool. + let is_first_block = is_first_block_in_core_with_digest(&digest).unwrap_or(true); + + if transaction_index.unwrap_or_default().saturating_sub(first_transaction_index.unwrap_or_default()) < MAX_TRANSACTION_TO_CONSIDER + && is_first_block && class_allowed { + log::trace!( + target: LOG_TARGET, + "Enabling `PotentialFullCore` mode for extrinsic", + ); + + *mode = Some(BlockWeightMode::::potential_full_core ( + // While applying inherents `extrinsic_index` and `first_transaction_index` will be `None`. + // When the first transaction is applied, we want to store the index. + first_transaction_index.or(transaction_index), + target_weight, + )); + } else { + log::trace!( + target: LOG_TARGET, + "Transaction is over the block limit, but is either outside of the allowed window or the dispatch class is not allowed.", + ); + + return Err(InvalidTransaction::ExhaustsResources) + } + } else if is_potential { + log::trace!( + target: LOG_TARGET, + "Resetting back to `FractionOfCore`" + ); + *mode = + Some(BlockWeightMode::::fraction_of_core(first_transaction_index.or(transaction_index))); + } else { + log::trace!( + target: LOG_TARGET, + "Not changing block weight mode" + ); + + *mode = + Some(BlockWeightMode::::fraction_of_core(first_transaction_index.or(transaction_index))); + } + }, + }; + + Ok(()) + }).map_err(Into::into) + } + + /// Should be called after all inner extensions have finished executing their post dispatch + /// handling. + /// + /// Returns the weight to refund. Aka the weight that wasn't used by this extension. + fn post_dispatch_extrinsic(info: &DispatchInfo) -> Weight { + crate::BlockWeightMode::::mutate(|weight_mode| { + let Some(mode) = weight_mode else { return Weight::zero() }; + + match mode { + // If the previous mode was already `FullCore`, we are fine. + BlockWeightMode::::FullCore { .. } => + Config::WeightInfo::block_weight_tx_extension_max_weight() + .saturating_sub(Config::WeightInfo::block_weight_tx_extension_full_core()), + BlockWeightMode::::FractionOfCore { .. } => { + let digest = frame_system::Pallet::::digest(); + let target_block_weight = + MaxParachainBlockWeight::::target_block_weight_with_digest(&digest); + + let is_above_limit = frame_system::Pallet::::remaining_block_weight() + .consumed() + .any_gt(target_block_weight); + + // If we are above the limit, it means the transaction used more weight than + // what it had announced, which should not happen. + if is_above_limit { + log::error!( + target: LOG_TARGET, + "Extrinsic ({}) used more weight than what it had announced and pushed the \ + block above the allowed weight limit!", + frame_system::Pallet::::extrinsic_index().unwrap_or_default() + ); + + // If this isn't the first block in a core, we register the full core weight + // to ensure that we don't include any other transactions. Because we don't + // know how many weight of the core was already used by the blocks before. + if !is_first_block_in_core_with_digest(&digest).unwrap_or(false) { + log::error!( + target: LOG_TARGET, + "Registering `FULL_CORE_WEIGHT` to ensure no other transaction is included \ + in this block, because this isn't the first block in the core!", + ); + + frame_system::Pallet::::register_extra_weight_unchecked( + FULL_CORE_WEIGHT, + DispatchClass::Mandatory, + ); + } + + *weight_mode = Some(BlockWeightMode::::full_core()); + + // Inform the node that this block uses the full core. + frame_system::Pallet::::deposit_log( + CumulusDigestItem::UseFullCore.to_digest_item(), + ); + } + + Config::WeightInfo::block_weight_tx_extension_max_weight().saturating_sub( + Config::WeightInfo::block_weight_tx_extension_stays_fraction_of_core(), + ) + }, + // Now we need to check if the transaction required more weight than a fraction of a + // core block. + BlockWeightMode::::PotentialFullCore { + first_transaction_index, + target_weight, + .. + } => { + let block_weight = frame_system::BlockWeight::::get(); + let extrinsic_class_weight = block_weight.get(info.class); + + if extrinsic_class_weight.any_gt(*target_weight) { + log::trace!( + target: LOG_TARGET, + "Extrinsic class weight {extrinsic_class_weight:?} above target weight {target_weight:?}, enabling `FullCore` mode." + ); + + *weight_mode = Some(BlockWeightMode::::full_core()); + + // Inform the node that this block uses the full core. + frame_system::Pallet::::deposit_log( + CumulusDigestItem::UseFullCore.to_digest_item(), + ); + } else { + log::trace!( + target: LOG_TARGET, + "Extrinsic class weight {extrinsic_class_weight:?} not above target \ + weight {target_weight:?}, going back to `FractionOfCore` mode." + ); + + *weight_mode = Some(BlockWeightMode::::fraction_of_core( + *first_transaction_index, + )); + } + + // We run into the worst case, so no refund :) + Weight::zero() + }, + } + }) + } +} + +impl< + Config, + Inner, + TargetBlockRate, + const MAX_TRANSACTION_TO_CONSIDER: u32, + const ALLOW_NORMAL: bool, + > From + for DynamicMaxBlockWeight< + Config, + Inner, + TargetBlockRate, + MAX_TRANSACTION_TO_CONSIDER, + ALLOW_NORMAL, + > +{ + fn from(s: Inner) -> Self { + Self::new(s) + } +} + +impl< + Config, + Inner: core::fmt::Debug, + TargetBlockRate, + const MAX_TRANSACTION_TO_CONSIDER: u32, + const ALLOW_NORMAL: bool, + > core::fmt::Debug + for DynamicMaxBlockWeight< + Config, + Inner, + TargetBlockRate, + MAX_TRANSACTION_TO_CONSIDER, + ALLOW_NORMAL, + > +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + write!(f, "DynamicMaxBlockWeight<{:?}>", self.0) + } +} + +impl< + Config: crate::Config + Send + Sync, + Inner: TransactionExtension, + TargetBlockRate: Get + Send + Sync + 'static, + const MAX_TRANSACTION_TO_CONSIDER: u32, + const ALLOW_NORMAL: bool, + > TransactionExtension + for DynamicMaxBlockWeight< + Config, + Inner, + TargetBlockRate, + MAX_TRANSACTION_TO_CONSIDER, + ALLOW_NORMAL, + > +where + Config::RuntimeCall: Dispatchable, +{ + const IDENTIFIER: &'static str = "DynamicMaxBlockWeight"; + + type Implicit = Inner::Implicit; + + type Val = Inner::Val; + + type Pre = Inner::Pre; + + fn implicit(&self) -> Result { + self.0.implicit() + } + + fn metadata() -> Vec { + let mut inner = Inner::metadata(); + inner.push(sp_runtime::traits::TransactionExtensionMetadata { + identifier: "DynamicMaxBlockWeight", + ty: scale_info::meta_type::<()>(), + implicit: scale_info::meta_type::<()>(), + }); + inner + } + + fn weight(&self, call: &Config::RuntimeCall) -> Weight { + Config::WeightInfo::block_weight_tx_extension_max_weight() + .saturating_add(self.0.weight(call)) + } + + fn validate( + &self, + origin: Config::RuntimeOrigin, + call: &Config::RuntimeCall, + info: &DispatchInfoOf, + len: usize, + self_implicit: Self::Implicit, + inherited_implication: &impl Implication, + source: TransactionSource, + ) -> Result<(ValidTransaction, Self::Val, Config::RuntimeOrigin), TransactionValidityError> { + Self::pre_validate_extrinsic(info, len)?; + + self.0 + .validate(origin, call, info, len, self_implicit, inherited_implication, source) + } + + fn prepare( + self, + val: Self::Val, + origin: &Config::RuntimeOrigin, + call: &Config::RuntimeCall, + info: &DispatchInfoOf, + len: usize, + ) -> Result { + self.0.prepare(val, origin, call, info, len) + } + + fn post_dispatch_details( + pre: Self::Pre, + info: &DispatchInfoOf, + post_info: &PostDispatchInfo, + len: usize, + result: &DispatchResult, + ) -> Result { + let weight_refund = Inner::post_dispatch_details(pre, info, post_info, len, result)?; + + let extra_refund = Self::post_dispatch_extrinsic(info); + + Ok(weight_refund.saturating_add(extra_refund)) + } + + fn bare_validate( + call: &Config::RuntimeCall, + info: &DispatchInfoOf, + len: usize, + ) -> frame_support::pallet_prelude::TransactionValidity { + Self::pre_validate_extrinsic(info, len)?; + + Inner::bare_validate(call, info, len) + } + + fn bare_validate_and_prepare( + call: &Config::RuntimeCall, + info: &DispatchInfoOf, + len: usize, + ) -> Result<(), TransactionValidityError> { + Self::pre_validate_extrinsic(info, len)?; + + Inner::bare_validate_and_prepare(call, info, len) + } + + fn bare_post_dispatch( + info: &DispatchInfoOf, + post_info: &mut PostDispatchInfoOf, + len: usize, + result: &DispatchResult, + ) -> Result<(), TransactionValidityError> { + Inner::bare_post_dispatch(info, post_info, len, result)?; + + Self::post_dispatch_extrinsic(info); + + Ok(()) + } +} diff --git a/cumulus/pallets/parachain-system/src/lib.rs b/cumulus/pallets/parachain-system/src/lib.rs index 85d12ed473467..e2f23f8dfb61a 100644 --- a/cumulus/pallets/parachain-system/src/lib.rs +++ b/cumulus/pallets/parachain-system/src/lib.rs @@ -63,18 +63,15 @@ use xcm::{latest::XcmHash, VersionedLocation, VersionedXcm, MAX_XCM_DECODE_DEPTH use xcm_builder::InspectMessageQueues; mod benchmarking; +pub mod block_weight; +pub mod consensus_hook; pub mod migration; mod mock; +pub mod relay_state_snapshot; #[cfg(test)] mod tests; -pub mod weights; - -pub use weights::WeightInfo; - mod unincluded_segment; - -pub mod consensus_hook; -pub mod relay_state_snapshot; +pub mod weights; #[macro_use] pub mod validate_block; mod descendant_validation; @@ -108,10 +105,11 @@ pub use consensus_hook::{ConsensusHook, ExpectParentIncluded}; pub use cumulus_pallet_parachain_system_proc_macro::register_validate_block; pub use relay_state_snapshot::{MessagingStateSnapshot, RelayChainStateProof}; pub use unincluded_segment::{Ancestor, UsedBandwidth}; +pub use weights::WeightInfo; pub use pallet::*; -const LOG_TARGET: &str = "parachain-system"; +const LOG_TARGET: &str = "runtime::parachain-system"; /// Something that can check the associated relay block number. /// @@ -188,8 +186,9 @@ pub mod ump_constants { #[frame_support::pallet] pub mod pallet { use super::*; + use codec::Compact; use cumulus_primitives_core::CoreInfoExistsAtMaxOnce; - use frame_support::pallet_prelude::*; + use frame_support::pallet_prelude::{ValueQuery, *}; use frame_system::pallet_prelude::*; #[pallet::pallet] @@ -369,6 +368,11 @@ pub mod pallet { UMPSignal::SelectCore(core_info.selector, core_info.claim_queue_offset) .encode(), ); + + PreviousCoreCount::::put(core_info.number_of_cores); + } else { + // Without the digest, we assume that it is `1`. + PreviousCoreCount::::put(Compact(1u16)); } // Send the pending UMP signals. @@ -482,6 +486,8 @@ pub mod pallet { weight += T::DbWeight::get().reads_writes(3, 2); } + BlockWeightMode::::kill(); + // Remove the validation from the old block. ValidationData::::kill(); // NOTE: Killing here is required to at least include the trie nodes down to the keys @@ -776,6 +782,24 @@ pub mod pallet { NotScheduled, } + /// The current block weight mode. + /// + /// This is used to determine what is the maximum allowed block weight, for more information see + /// [`block_weight`]. + /// + /// Killed in [`Self::on_initialize`] and set by the [`block_weight`] logic. + #[pallet::storage] + #[pallet::whitelist_storage] + pub type BlockWeightMode = + StorageValue<_, block_weight::BlockWeightMode, OptionQuery>; + + /// The core count available to the parachain in the previous block. + /// + /// This is mainly used for offchain functionality to calculate the correct target block weight. + #[pallet::storage] + #[pallet::whitelist_storage] + pub type PreviousCoreCount = StorageValue<_, Compact, OptionQuery>; + /// Latest included block descendants the runtime accepted. In other words, these are /// ancestors of the currently executing block which have not been included in the observed /// relay-chain state. @@ -1406,7 +1430,11 @@ impl Pallet { // // If this fails, the parachain needs to wait for ancestors to be included before // a new block is allowed. - assert!(new_len < capacity.get(), "no space left for the block in the unincluded segment"); + assert!( + new_len < capacity.get(), + "No space left for the block in the unincluded segment: new_len({new_len}) < capacity({})", + capacity.get() + ); weight_used } @@ -1470,7 +1498,7 @@ impl Pallet { // Ensure that `ValidationData` exists. We do not care about the validation data per se, // but we do care about the [`UpgradeRestrictionSignal`] which arrives with the same // inherent. - ensure!(>::exists(), Error::::ValidationDataNotAvailable,); + ensure!(>::exists(), Error::::ValidationDataNotAvailable); ensure!(>::get().is_none(), Error::::ProhibitedByPolkadot); ensure!(!>::exists(), Error::::OverlappingUpgrades); diff --git a/cumulus/pallets/parachain-system/src/tests.rs b/cumulus/pallets/parachain-system/src/tests.rs index 686e254bf4319..dc9fab04f706f 100755 --- a/cumulus/pallets/parachain-system/src/tests.rs +++ b/cumulus/pallets/parachain-system/src/tests.rs @@ -185,7 +185,7 @@ fn unincluded_segment_works() { } #[test] -#[should_panic = "no space left for the block in the unincluded segment"] +#[should_panic = "No space left for the block in the unincluded segment: new_len(1) < capacity(1)"] fn unincluded_segment_is_limited() { CONSENSUS_HOOK.with(|c| { *c.borrow_mut() = Box::new(|_| (Weight::zero(), NonZeroU32::new(1).unwrap().into())) diff --git a/cumulus/pallets/parachain-system/src/validate_block/implementation.rs b/cumulus/pallets/parachain-system/src/validate_block/implementation.rs index 29905d921f375..22f1154517d96 100644 --- a/cumulus/pallets/parachain-system/src/validate_block/implementation.rs +++ b/cumulus/pallets/parachain-system/src/validate_block/implementation.rs @@ -339,6 +339,7 @@ where upward_messages .try_push(UMP_SEPARATOR) .expect("UMPSignals does not fit in UMPMessages"); + upward_messages .try_extend(upward_message_signals.into_iter()) .expect("UMPSignals does not fit in UMPMessages"); diff --git a/cumulus/pallets/parachain-system/src/validate_block/tests.rs b/cumulus/pallets/parachain-system/src/validate_block/tests.rs index 2b955c4514309..210ec48bf5e1e 100644 --- a/cumulus/pallets/parachain-system/src/validate_block/tests.rs +++ b/cumulus/pallets/parachain-system/src/validate_block/tests.rs @@ -31,14 +31,14 @@ use cumulus_test_client::{ use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; use polkadot_parachain_primitives::primitives::ValidationResult; use sc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy}; -use sp_api::{ApiExt, Core, ProofRecorder, ProvideRuntimeApi}; -use sp_consensus_slots::SlotDuration; +use sp_api::{ApiExt, Core, ProofRecorder, ProvideRuntimeApi, StorageProof}; +use sp_consensus_babe::SlotDuration; use sp_core::H256; use sp_runtime::{ traits::{BlakeTwo256, Block as BlockT, Header as HeaderT}, DigestItem, }; -use sp_trie::{proof_size_extension::ProofSizeExt, recorder::IgnoredNodes, StorageProof}; +use sp_trie::{proof_size_extension::ProofSizeExt, recorder::IgnoredNodes}; use std::{env, process::Command}; fn call_validate_block_validation_result( @@ -148,6 +148,7 @@ fn build_block_with_witness( let cumulus_test_client::BlockBuilderAndSupportData { mut block_builder, persisted_validation_data, + .. } = client.init_block_builder_with_pre_digests(Some(validation_data), sproof_builder, pre_digests); extra_extrinsics.into_iter().for_each(|e| block_builder.push(e).unwrap()); @@ -242,11 +243,10 @@ fn build_multiple_blocks_with_witness( }) .unwrap(); - ignored_nodes.extend(IgnoredNodes::from_storage_proof::( - &built_block.proof.clone().unwrap(), - )); + let proof_new = built_block.proof.unwrap(); + ignored_nodes.extend(IgnoredNodes::from_storage_proof::(&proof_new)); ignored_nodes.extend(IgnoredNodes::from_memory_db(built_block.storage_changes.transaction)); - proof = StorageProof::merge([proof, built_block.proof.unwrap()]); + proof = StorageProof::merge([proof, proof_new]); parent_head = built_block.block.header.clone(); @@ -519,7 +519,6 @@ fn state_changes_in_multiple_blocks_are_applied_in_exact_order() { sp_tracing::try_init_simple(); let blocks_per_pov = 12; - // disable the core selection logic let (client, genesis_head) = create_elastic_scaling_test_client(); // 1. Build the initial block that stores values in the map. @@ -584,7 +583,6 @@ fn validate_block_handles_ump_signal() { relay_chain::{UMPSignal, UMP_SEPARATOR}, ClaimQueueOffset, CoreInfo, CoreSelector, }; - sp_tracing::try_init_simple(); let (client, parent_head) = create_elastic_scaling_test_client(); diff --git a/cumulus/pallets/parachain-system/src/weights.rs b/cumulus/pallets/parachain-system/src/weights.rs index ba7d8b1e87f6b..086a6b993b695 100644 --- a/cumulus/pallets/parachain-system/src/weights.rs +++ b/cumulus/pallets/parachain-system/src/weights.rs @@ -55,6 +55,9 @@ use core::marker::PhantomData; /// Weight functions needed for cumulus_pallet_parachain_system. pub trait WeightInfo { fn enqueue_inbound_downward_messages(n: u32, ) -> Weight; + fn block_weight_tx_extension_max_weight() -> Weight; + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight; + fn block_weight_tx_extension_full_core() -> Weight; } /// Weights for cumulus_pallet_parachain_system using the Substrate node and recommended hardware. @@ -84,6 +87,18 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } + + fn block_weight_tx_extension_max_weight() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_full_core() -> Weight { + Weight::zero() + } } // For backwards compatibility and tests @@ -112,4 +127,17 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } + + fn block_weight_tx_extension_max_weight() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_full_core() -> Weight { + Weight::zero() + } + } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/cumulus_pallet_parachain_system.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/cumulus_pallet_parachain_system.rs index 23dd800922aea..b7ba28ac2fbcc 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/cumulus_pallet_parachain_system.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/cumulus_pallet_parachain_system.rs @@ -74,4 +74,15 @@ impl cumulus_pallet_parachain_system::WeightInfo for We .saturating_add(T::DbWeight::get().writes(4)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) } + fn block_weight_tx_extension_max_weight() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_full_core() -> Weight { + Weight::zero() + } } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/cumulus_pallet_parachain_system.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/cumulus_pallet_parachain_system.rs index 28f8aca5f5e7e..2573210408d95 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/cumulus_pallet_parachain_system.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/cumulus_pallet_parachain_system.rs @@ -74,4 +74,15 @@ impl cumulus_pallet_parachain_system::WeightInfo for We .saturating_add(T::DbWeight::get().writes(4)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) } + fn block_weight_tx_extension_max_weight() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_full_core() -> Weight { + Weight::zero() + } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/cumulus_pallet_parachain_system.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/cumulus_pallet_parachain_system.rs index 145a6e3e3cf1b..ebb497e21decd 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/cumulus_pallet_parachain_system.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/cumulus_pallet_parachain_system.rs @@ -74,4 +74,15 @@ impl cumulus_pallet_parachain_system::WeightInfo for We .saturating_add(T::DbWeight::get().writes(4)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) } + fn block_weight_tx_extension_max_weight() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_full_core() -> Weight { + Weight::zero() + } } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/cumulus_pallet_parachain_system.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/cumulus_pallet_parachain_system.rs index e60c9cfde30e5..5507c5ffd7f2a 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/cumulus_pallet_parachain_system.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/cumulus_pallet_parachain_system.rs @@ -74,4 +74,15 @@ impl cumulus_pallet_parachain_system::WeightInfo for We .saturating_add(T::DbWeight::get().writes(4)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) } + fn block_weight_tx_extension_max_weight() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_full_core() -> Weight { + Weight::zero() + } } diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/cumulus_pallet_parachain_system.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/cumulus_pallet_parachain_system.rs index 9ebfbd2fbd0a3..671e4715b8219 100644 --- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/cumulus_pallet_parachain_system.rs +++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/weights/cumulus_pallet_parachain_system.rs @@ -74,4 +74,15 @@ impl cumulus_pallet_parachain_system::WeightInfo for We .saturating_add(T::DbWeight::get().writes(4)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) } + fn block_weight_tx_extension_max_weight() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_full_core() -> Weight { + Weight::zero() + } } diff --git a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/weights/cumulus_pallet_parachain_system.rs b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/weights/cumulus_pallet_parachain_system.rs index 73c4b2ba241d2..bc702a215a66a 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/weights/cumulus_pallet_parachain_system.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/weights/cumulus_pallet_parachain_system.rs @@ -74,4 +74,15 @@ impl cumulus_pallet_parachain_system::WeightInfo for We .saturating_add(T::DbWeight::get().writes(4)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) } + fn block_weight_tx_extension_max_weight() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_full_core() -> Weight { + Weight::zero() + } } diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/src/weights/cumulus_pallet_parachain_system.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/weights/cumulus_pallet_parachain_system.rs index 8f5714bbe0cd7..ece41c94ab847 100644 --- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/weights/cumulus_pallet_parachain_system.rs +++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/weights/cumulus_pallet_parachain_system.rs @@ -74,4 +74,15 @@ impl cumulus_pallet_parachain_system::WeightInfo for We .saturating_add(T::DbWeight::get().writes(4)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) } + fn block_weight_tx_extension_max_weight() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_full_core() -> Weight { + Weight::zero() + } } diff --git a/cumulus/parachains/runtimes/glutton/glutton-westend/src/weights/cumulus_pallet_parachain_system.rs b/cumulus/parachains/runtimes/glutton/glutton-westend/src/weights/cumulus_pallet_parachain_system.rs index a753f6fc78f87..4c8cc2b5fc6f8 100644 --- a/cumulus/parachains/runtimes/glutton/glutton-westend/src/weights/cumulus_pallet_parachain_system.rs +++ b/cumulus/parachains/runtimes/glutton/glutton-westend/src/weights/cumulus_pallet_parachain_system.rs @@ -75,4 +75,15 @@ impl cumulus_pallet_parachain_system::WeightInfo for We .saturating_add(T::DbWeight::get().writes(4)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) } + fn block_weight_tx_extension_max_weight() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_full_core() -> Weight { + Weight::zero() + } } diff --git a/cumulus/parachains/runtimes/people/people-rococo/src/weights/cumulus_pallet_parachain_system.rs b/cumulus/parachains/runtimes/people/people-rococo/src/weights/cumulus_pallet_parachain_system.rs index 58aef8cd5ab87..6e7e6acc40e3b 100644 --- a/cumulus/parachains/runtimes/people/people-rococo/src/weights/cumulus_pallet_parachain_system.rs +++ b/cumulus/parachains/runtimes/people/people-rococo/src/weights/cumulus_pallet_parachain_system.rs @@ -74,4 +74,15 @@ impl cumulus_pallet_parachain_system::WeightInfo for We .saturating_add(T::DbWeight::get().writes(4)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) } + fn block_weight_tx_extension_max_weight() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_full_core() -> Weight { + Weight::zero() + } } diff --git a/cumulus/parachains/runtimes/people/people-westend/src/weights/cumulus_pallet_parachain_system.rs b/cumulus/parachains/runtimes/people/people-westend/src/weights/cumulus_pallet_parachain_system.rs index 05c07f998e8e2..085b4b0fa85e7 100644 --- a/cumulus/parachains/runtimes/people/people-westend/src/weights/cumulus_pallet_parachain_system.rs +++ b/cumulus/parachains/runtimes/people/people-westend/src/weights/cumulus_pallet_parachain_system.rs @@ -74,4 +74,15 @@ impl cumulus_pallet_parachain_system::WeightInfo for We .saturating_add(T::DbWeight::get().writes(4)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) } + fn block_weight_tx_extension_max_weight() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_full_core() -> Weight { + Weight::zero() + } } diff --git a/cumulus/primitives/core/src/lib.rs b/cumulus/primitives/core/src/lib.rs index e06a92dcef8bc..03f337b279671 100644 --- a/cumulus/primitives/core/src/lib.rs +++ b/cumulus/primitives/core/src/lib.rs @@ -231,6 +231,43 @@ pub struct CoreInfo { pub number_of_cores: Compact, } +impl core::hash::Hash for CoreInfo { + fn hash(&self, state: &mut H) { + state.write_u8(self.selector.0); + state.write_u8(self.claim_queue_offset.0); + state.write_u16(self.number_of_cores.0); + } +} + +impl CoreInfo { + /// Puts this into a [`CumulusDigestItem::CoreInfo`] and then encodes it as a Substrate + /// [`DigestItem`]. + pub fn to_digest_item(&self) -> DigestItem { + CumulusDigestItem::CoreInfo(self.clone()).to_digest_item() + } +} + +/// Information about a block that is part of a PoV bundle. +#[derive(Clone, Debug, Decode, Encode, PartialEq)] +pub struct BundleInfo { + /// The index of the block in the bundle. + pub index: u8, + /// Is this the last block in the bundle from the point of view of the node? + /// + /// It is possible that at `index` zero the runtime outputs the + /// [`CumulusDigestItem::UseFullCore`] that informs the node to use an entire for one block + /// only. + pub maybe_last: bool, +} + +impl BundleInfo { + /// Puts this into a [`CumulusDigestItem::BundleInfo`] and then encodes it as a Substrate + /// [`DigestItem`]. + pub fn to_digest_item(&self) -> DigestItem { + CumulusDigestItem::BundleInfo(self.clone()).to_digest_item() + } +} + /// Return value of [`CumulusDigestItem::core_info_exists_at_max_once`] #[derive(Debug, Clone, PartialEq, Eq)] pub enum CoreInfoExistsAtMaxOnce { @@ -261,14 +298,25 @@ pub enum CumulusDigestItem { /// block. #[codec(index = 1)] CoreInfo(CoreInfo), + /// A digest item providing information about the position of the block in the bundle. + #[codec(index = 2)] + BundleInfo(BundleInfo), + /// A digest item informing the node that this block should be put alone onto a core. + /// + /// In other words, the core should not be shared with other blocks. + #[codec(index = 3)] + UseFullCore, } impl CumulusDigestItem { /// Encode this as a Substrate [`DigestItem`]. pub fn to_digest_item(&self) -> DigestItem { + let encoded = self.encode(); + match self { - Self::RelayParent(_) => DigestItem::Consensus(CUMULUS_CONSENSUS_ID, self.encode()), - Self::CoreInfo(_) => DigestItem::PreRuntime(CUMULUS_CONSENSUS_ID, self.encode()), + Self::RelayParent(_) | Self::UseFullCore => + DigestItem::Consensus(CUMULUS_CONSENSUS_ID, encoded), + _ => DigestItem::PreRuntime(CUMULUS_CONSENSUS_ID, encoded), } } @@ -347,6 +395,40 @@ impl CumulusDigestItem { _ => None, }) } + + /// Returns the [`BundleInfo`] from the given `digest`. + pub fn find_bundle_info(digest: &Digest) -> Option { + digest.convert_first(|d| match d { + DigestItem::PreRuntime(id, val) if id == &CUMULUS_CONSENSUS_ID => { + let Ok(CumulusDigestItem::BundleInfo(bundle_info)) = + CumulusDigestItem::decode_all(&mut &val[..]) + else { + return None + }; + + Some(bundle_info) + }, + _ => None, + }) + } + + /// Returns `true` if the given `digest` contains the [`Self::UseFullCore`] item. + pub fn contains_use_full_core(digest: &Digest) -> bool { + digest + .convert_first(|d| match d { + DigestItem::Consensus(id, val) if id == &CUMULUS_CONSENSUS_ID => { + let Ok(CumulusDigestItem::UseFullCore) = + CumulusDigestItem::decode_all(&mut &val[..]) + else { + return None + }; + + Some(true) + }, + _ => None, + }) + .unwrap_or_default() + } } /// diff --git a/polkadot/primitives/src/v9/mod.rs b/polkadot/primitives/src/v9/mod.rs index 360da8ff9b956..c55edd47c3c19 100644 --- a/polkadot/primitives/src/v9/mod.rs +++ b/polkadot/primitives/src/v9/mod.rs @@ -2238,10 +2238,22 @@ impl Ord for CommittedCandidateReceiptV2 { #[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, Debug, Copy)] pub struct CoreSelector(pub u8); +impl From for CoreSelector { + fn from(value: u8) -> Self { + Self(value) + } +} + /// An offset in the relay chain claim queue. #[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, Debug, Copy)] pub struct ClaimQueueOffset(pub u8); +impl From for ClaimQueueOffset { + fn from(value: u8) -> Self { + Self(value) + } +} + /// Signals that a parachain can send to the relay chain via the UMP queue. #[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, Debug)] pub enum UMPSignal { diff --git a/substrate/frame/staking-async/runtimes/parachain/src/weights/cumulus_pallet_parachain_system.rs b/substrate/frame/staking-async/runtimes/parachain/src/weights/cumulus_pallet_parachain_system.rs index b91921ce85eb1..55bd6a43a6a0f 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/weights/cumulus_pallet_parachain_system.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/weights/cumulus_pallet_parachain_system.rs @@ -79,4 +79,15 @@ impl cumulus_pallet_parachain_system::WeightInfo for We .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) } + fn block_weight_tx_extension_max_weight() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_stays_fraction_of_core() -> Weight { + Weight::zero() + } + + fn block_weight_tx_extension_full_core() -> Weight { + Weight::zero() + } } diff --git a/substrate/frame/support/src/traits/hooks.rs b/substrate/frame/support/src/traits/hooks.rs index f7404ca7dbe85..b4027547c47e5 100644 --- a/substrate/frame/support/src/traits/hooks.rs +++ b/substrate/frame/support/src/traits/hooks.rs @@ -130,6 +130,11 @@ impl_for_tuples_attr! { &[for_tuples!( #( Tuple::on_idle ),* )]; let mut weight = Weight::zero(); let len = on_idle_functions.len(); + + if len == 0 { + return Weight::zero() + } + let start_index = n % (len as u32).into(); let start_index = start_index.try_into().ok().expect( "`start_index % len` always fits into `usize`, because `len` can be in maximum `usize::MAX`; qed" diff --git a/substrate/frame/support/src/traits/messages.rs b/substrate/frame/support/src/traits/messages.rs index 0a5c70f8f0fa5..eefe47ff53a61 100644 --- a/substrate/frame/support/src/traits/messages.rs +++ b/substrate/frame/support/src/traits/messages.rs @@ -356,6 +356,16 @@ pub trait HandleMessage { fn sweep_queue(); } +impl HandleMessage for () { + type MaxMessageLen = ConstU32<0>; + + fn handle_message(_: BoundedSlice) {} + + fn handle_messages<'a>(_: impl Iterator>) {} + + fn sweep_queue() {} +} + /// Adapter type to transform an [`EnqueueMessage`] with an origin into a [`HandleMessage`] impl. pub struct EnqueueWithOrigin(PhantomData<(E, O)>); impl, O: TypedGet> HandleMessage for EnqueueWithOrigin diff --git a/substrate/frame/system/src/extensions/check_weight.rs b/substrate/frame/system/src/extensions/check_weight.rs index 16522611ca474..b64f68cb71b82 100644 --- a/substrate/frame/system/src/extensions/check_weight.rs +++ b/substrate/frame/system/src/extensions/check_weight.rs @@ -38,10 +38,16 @@ use sp_weights::Weight; /// /// This extension does not influence any fields of `TransactionValidity` in case the /// transaction is valid. -#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, Default, TypeInfo)] +#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)] #[scale_info(skip_type_params(T))] pub struct CheckWeight(core::marker::PhantomData); +impl Default for CheckWeight { + fn default() -> Self { + Self(Default::default()) + } +} + impl CheckWeight where T::RuntimeCall: Dispatchable, diff --git a/substrate/frame/system/src/lib.rs b/substrate/frame/system/src/lib.rs index 9d80e6ec338d2..a1924219a8aaa 100644 --- a/substrate/frame/system/src/lib.rs +++ b/substrate/frame/system/src/lib.rs @@ -1884,6 +1884,11 @@ impl Pallet { AllExtrinsicsLen::::get().unwrap_or_default() } + /// Returns the current active execution phase. + pub fn execution_phase() -> Option { + ExecutionPhase::::get() + } + /// Inform the system pallet of some additional weight that should be accounted for, in the /// current block. /// @@ -2154,7 +2159,7 @@ impl Pallet { } /// Sets the index of extrinsic that is currently executing. - #[cfg(any(feature = "std", test))] + #[cfg(any(feature = "std", feature = "runtime-benchmarks", test))] pub fn set_extrinsic_index(extrinsic_index: u32) { storage::unhashed::put(well_known_keys::EXTRINSIC_INDEX, &extrinsic_index) } diff --git a/substrate/primitives/runtime/src/testing.rs b/substrate/primitives/runtime/src/testing.rs index 647f5eb78d5e1..59f71d4ce4a9a 100644 --- a/substrate/primitives/runtime/src/testing.rs +++ b/substrate/primitives/runtime/src/testing.rs @@ -21,7 +21,7 @@ use crate::{ codec::{Codec, Decode, DecodeWithMemTracking, Encode, EncodeLike, MaxEncodedLen}, generic::{self, LazyBlock, UncheckedExtrinsic}, scale_info::TypeInfo, - traits::{self, BlakeTwo256, Dispatchable, LazyExtrinsic, OpaqueKeys}, + traits::{self, BlakeTwo256, Dispatchable, LazyExtrinsic, Lookup, OpaqueKeys, StaticLookup}, DispatchResultWithInfo, KeyTypeId, OpaqueExtrinsic, }; use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize}; @@ -54,6 +54,12 @@ use std::{cell::RefCell, fmt::Debug}; )] pub struct UintAuthorityId(pub u64); +impl core::fmt::Display for UintAuthorityId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&self.0, f) + } +} + impl From for UintAuthorityId { fn from(id: u64) -> Self { UintAuthorityId(id) @@ -162,6 +168,28 @@ impl traits::IdentifyAccount for UintAuthorityId { } } +impl StaticLookup for UintAuthorityId { + type Source = Self; + type Target = u64; + + fn lookup(s: Self::Source) -> Result { + Ok(s.0) + } + + fn unlookup(t: Self::Target) -> Self::Source { + Self(t) + } +} + +impl Lookup for UintAuthorityId { + type Source = Self; + type Target = u64; + + fn lookup(&self, s: Self::Source) -> Result { + Ok(s.0) + } +} + impl traits::Verify for UintAuthorityId { type Signer = Self;