From 3ea497b5a0fdda252f9c5a3c257cfaf8685f02fd Mon Sep 17 00:00:00 2001 From: asynchronous rob <rphmeier@gmail.com> Date: Tue, 3 Oct 2023 09:23:22 -0500 Subject: [PATCH 01/54] expose the last relay chain block number as an API from parachain-system (#1761) re: https://forum.polkadot.network/t/blocknumber-vs-timestamps-should-we-abandon-blocktimes-altogether/4077 This exposes the `LastRelayChainBlockNumber` storage member of `cumulus-pallet-parachain-system` with a getter and alters the behavior of this storage item to only be updated in `on_finalize` to ensure a consistent value throughout `on_initialize` and within transactions. Parachains, especially with features such as asynchronous backing and agile coretime, should not use the parachain block number as a clock. Any feature of Polkadot intended to optimize core utilization and parachain coretime consumption is likely to worsen this clock as it is practically applied. --- cumulus/pallets/parachain-system/src/lib.rs | 23 ++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/cumulus/pallets/parachain-system/src/lib.rs b/cumulus/pallets/parachain-system/src/lib.rs index a8f0a49223f53..eaf15768e290d 100644 --- a/cumulus/pallets/parachain-system/src/lib.rs +++ b/cumulus/pallets/parachain-system/src/lib.rs @@ -245,10 +245,10 @@ pub mod pallet { <UpgradeRestrictionSignal<T>>::kill(); let relay_upgrade_go_ahead = <UpgradeGoAhead<T>>::take(); - assert!( - <ValidationData<T>>::exists(), - "set_validation_data inherent needs to be present in every block!" - ); + let vfp = <ValidationData<T>>::get() + .expect("set_validation_data inherent needs to be present in every block!"); + + LastRelayChainBlockNumber::<T>::put(vfp.relay_parent_number); let host_config = match Self::host_configuration() { Some(ok) => ok, @@ -380,8 +380,7 @@ pub mod pallet { let ancestor = Ancestor::new_unchecked(used_bandwidth, consumed_go_ahead_signal); let watermark = HrmpWatermark::<T>::get(); - let watermark_update = - HrmpWatermarkUpdate::new(watermark, LastRelayChainBlockNumber::<T>::get()); + let watermark_update = HrmpWatermarkUpdate::new(watermark, vfp.relay_parent_number); aggregated_segment .append(&ancestor, watermark_update, &total_bandwidth_out) @@ -460,6 +459,9 @@ pub mod pallet { 4 + hrmp_max_message_num_per_candidate as u64, ); + // Weight for updating the last relay chain block number in `on_finalize`. + weight += T::DbWeight::get().reads_writes(1, 1); + // Weight for adjusting the unincluded segment in `on_finalize`. weight += T::DbWeight::get().reads_writes(6, 3); @@ -515,7 +517,6 @@ pub mod pallet { vfp.relay_parent_number, LastRelayChainBlockNumber::<T>::get(), ); - LastRelayChainBlockNumber::<T>::put(vfp.relay_parent_number); let relay_state_proof = RelayChainStateProof::new( T::SelfParaId::get(), @@ -756,6 +757,8 @@ pub mod pallet { pub(super) type DidSetValidationCode<T: Config> = StorageValue<_, bool, ValueQuery>; /// The relay chain block number associated with the last parachain block. + /// + /// This is updated in `on_finalize`. #[pallet::storage] pub(super) type LastRelayChainBlockNumber<T: Config> = StorageValue<_, RelayChainBlockNumber, ValueQuery>; @@ -1501,6 +1504,12 @@ impl<T: Config> Pallet<T> { Self::deposit_event(Event::UpwardMessageSent { message_hash: Some(hash) }); Ok((0, hash)) } + + /// Get the relay chain block number which was used as an anchor for the last block in this + /// chain. + pub fn last_relay_block_number(&self) -> RelayChainBlockNumber { + LastRelayChainBlockNumber::<T>::get() + } } impl<T: Config> UpwardMessageSender for Pallet<T> { From aad80cce318fd492ce0d9c9951f2d7ec949710f3 Mon Sep 17 00:00:00 2001 From: yjh <yjh465402634@gmail.com> Date: Tue, 3 Oct 2023 23:16:46 +0800 Subject: [PATCH 02/54] feat: compute pallet/storage prefix hash at compile time (#1539) Since the hash rules of this part of the `pallet_prefix/storage_prefix` are always fixed, we can put the runtime calculation into compile time. --- polkadot address: 15ouFh2SHpGbHtDPsJ6cXQfes9Cx1gEFnJJsJVqPGzBSTudr --------- Co-authored-by: Juan <juangirini@gmail.com> Co-authored-by: command-bot <> Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> --- Cargo.lock | 1 + .../basic-authorship/src/basic_authorship.rs | 2 +- .../bags-list/remote-tests/src/snapshot.rs | 4 +- .../bags-list/remote-tests/src/try_state.rs | 4 +- substrate/frame/paged-list/src/paged_list.rs | 12 +++--- substrate/frame/support/procedural/Cargo.toml | 1 + .../procedural/src/construct_runtime/mod.rs | 38 ++++++++++++----- .../src/pallet/expand/pallet_struct.rs | 8 ++++ .../procedural/src/pallet/expand/storage.rs | 42 ++++++++++++++++++- .../support/procedural/src/pallet/mod.rs | 2 +- .../procedural/src/pallet/parse/helper.rs | 16 ++++++- .../support/procedural/src/storage_alias.rs | 11 ++++- .../src/storage/generator/double_map.rs | 29 ++++++------- .../support/src/storage/generator/map.rs | 27 ++++++------ .../support/src/storage/generator/nmap.rs | 31 +++++++------- .../support/src/storage/generator/value.rs | 18 ++++---- substrate/frame/support/src/storage/mod.rs | 32 ++++++++------ .../support/src/storage/types/counted_map.rs | 2 +- .../support/src/storage/types/counted_nmap.rs | 2 +- .../support/src/storage/types/double_map.rs | 15 ++++--- .../frame/support/src/storage/types/map.rs | 13 +++--- .../frame/support/src/storage/types/nmap.rs | 13 +++--- .../frame/support/src/storage/types/value.rs | 9 ++-- .../frame/support/src/tests/storage_alias.rs | 10 ++--- .../frame/support/src/traits/metadata.rs | 13 ++++++ substrate/frame/support/src/traits/storage.rs | 27 ++++++++++++ substrate/frame/tips/src/tests.rs | 2 +- substrate/frame/tx-pause/src/lib.rs | 2 +- 28 files changed, 263 insertions(+), 123 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0ca0b012c640..6d079c53b0f58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5289,6 +5289,7 @@ dependencies = [ "proc-macro-warning", "proc-macro2", "quote", + "sp-core-hashing", "syn 2.0.37", ] diff --git a/substrate/client/basic-authorship/src/basic_authorship.rs b/substrate/client/basic-authorship/src/basic_authorship.rs index 0fb61b6fab1f6..57c2996ab4063 100644 --- a/substrate/client/basic-authorship/src/basic_authorship.rs +++ b/substrate/client/basic-authorship/src/basic_authorship.rs @@ -79,7 +79,7 @@ pub struct ProposerFactory<A, B, C, PR> { /// The soft deadline indicates where we should stop attempting to add transactions /// to the block, which exhaust resources. After soft deadline is reached, /// we switch to a fixed-amount mode, in which after we see `MAX_SKIPPED_TRANSACTIONS` - /// transactions which exhaust resrouces, we will conclude that the block is full. + /// transactions which exhaust resources, we will conclude that the block is full. soft_deadline_percent: Percent, telemetry: Option<TelemetryHandle>, /// When estimating the block size, should the proof be included? diff --git a/substrate/frame/bags-list/remote-tests/src/snapshot.rs b/substrate/frame/bags-list/remote-tests/src/snapshot.rs index 13922cd3ca618..81a8905e6b4b2 100644 --- a/substrate/frame/bags-list/remote-tests/src/snapshot.rs +++ b/substrate/frame/bags-list/remote-tests/src/snapshot.rs @@ -42,8 +42,8 @@ where .to_string()], at: None, hashed_prefixes: vec![ - <pallet_staking::Bonded<Runtime>>::prefix_hash(), - <pallet_staking::Ledger<Runtime>>::prefix_hash(), + <pallet_staking::Bonded<Runtime>>::prefix_hash().to_vec(), + <pallet_staking::Ledger<Runtime>>::prefix_hash().to_vec(), <pallet_staking::Validators<Runtime>>::map_storage_final_prefix(), <pallet_staking::Nominators<Runtime>>::map_storage_final_prefix(), ], diff --git a/substrate/frame/bags-list/remote-tests/src/try_state.rs b/substrate/frame/bags-list/remote-tests/src/try_state.rs index 338be50a93f79..83930024c89a5 100644 --- a/substrate/frame/bags-list/remote-tests/src/try_state.rs +++ b/substrate/frame/bags-list/remote-tests/src/try_state.rs @@ -39,8 +39,8 @@ pub async fn execute<Runtime, Block>( pallets: vec![pallet_bags_list::Pallet::<Runtime, pallet_bags_list::Instance1>::name() .to_string()], hashed_prefixes: vec![ - <pallet_staking::Bonded<Runtime>>::prefix_hash(), - <pallet_staking::Ledger<Runtime>>::prefix_hash(), + <pallet_staking::Bonded<Runtime>>::prefix_hash().to_vec(), + <pallet_staking::Ledger<Runtime>>::prefix_hash().to_vec(), ], ..Default::default() })) diff --git a/substrate/frame/paged-list/src/paged_list.rs b/substrate/frame/paged-list/src/paged_list.rs index 3597c3dea6823..beea8ecc64409 100644 --- a/substrate/frame/paged-list/src/paged_list.rs +++ b/substrate/frame/paged-list/src/paged_list.rs @@ -53,7 +53,7 @@ pub type ValueIndex = u32; /// [`Page`]s. /// /// Each [`Page`] holds at most `ValuesPerNewPage` values in its `values` vector. The last page is -/// the only one that could have less than `ValuesPerNewPage` values. +/// the only one that could have less than `ValuesPerNewPage` values. /// **Iteration** happens by starting /// at [`first_page`][StoragePagedListMeta::first_page]/ /// [`first_value_offset`][StoragePagedListMeta::first_value_offset] and incrementing these indices @@ -373,11 +373,11 @@ where /// that are completely useless for prefix calculation. struct StoragePagedListPrefix<Prefix>(PhantomData<Prefix>); -impl<Prefix> frame_support::storage::StoragePrefixedContainer for StoragePagedListPrefix<Prefix> +impl<Prefix> StoragePrefixedContainer for StoragePagedListPrefix<Prefix> where Prefix: StorageInstance, { - fn module_prefix() -> &'static [u8] { + fn pallet_prefix() -> &'static [u8] { Prefix::pallet_prefix().as_bytes() } @@ -386,15 +386,15 @@ where } } -impl<Prefix, Value, ValuesPerNewPage> frame_support::storage::StoragePrefixedContainer +impl<Prefix, Value, ValuesPerNewPage> StoragePrefixedContainer for StoragePagedList<Prefix, Value, ValuesPerNewPage> where Prefix: StorageInstance, Value: FullCodec, ValuesPerNewPage: Get<u32>, { - fn module_prefix() -> &'static [u8] { - StoragePagedListPrefix::<Prefix>::module_prefix() + fn pallet_prefix() -> &'static [u8] { + StoragePagedListPrefix::<Prefix>::pallet_prefix() } fn storage_prefix() -> &'static [u8] { diff --git a/substrate/frame/support/procedural/Cargo.toml b/substrate/frame/support/procedural/Cargo.toml index 6381e430f2baa..704f355ff6c80 100644 --- a/substrate/frame/support/procedural/Cargo.toml +++ b/substrate/frame/support/procedural/Cargo.toml @@ -26,6 +26,7 @@ frame-support-procedural-tools = { path = "tools" } proc-macro-warning = { version = "0.4.2", default-features = false } macro_magic = { version = "0.4.2", features = ["proc_support"] } expander = "2.0.0" +sp-core-hashing = { path = "../../../primitives/core/hashing" } [features] default = [ "std" ] diff --git a/substrate/frame/support/procedural/src/construct_runtime/mod.rs b/substrate/frame/support/procedural/src/construct_runtime/mod.rs index f42dd837e3a95..e8c3c0889214c 100644 --- a/substrate/frame/support/procedural/src/construct_runtime/mod.rs +++ b/substrate/frame/support/procedural/src/construct_runtime/mod.rs @@ -211,6 +211,7 @@ mod expand; mod parse; +use crate::pallet::parse::helper::two128_str; use cfg_expr::Predicate; use frame_support_procedural_tools::{ generate_crate_access, generate_crate_access_2018, generate_hidden_includes, @@ -403,17 +404,19 @@ fn construct_runtime_final_expansion( let integrity_test = decl_integrity_test(&scrate); let static_assertions = decl_static_assertions(&name, &pallets, &scrate); - let warning = - where_section.map_or(None, |where_section| { - Some(proc_macro_warning::Warning::new_deprecated("WhereSection") - .old("use a `where` clause in `construct_runtime`") - .new("use `frame_system::Config` to set the `Block` type and delete this clause. - It is planned to be removed in December 2023") - .help_links(&["https://github.com/paritytech/substrate/pull/14437"]) - .span(where_section.span) - .build(), + let warning = where_section.map_or(None, |where_section| { + Some( + proc_macro_warning::Warning::new_deprecated("WhereSection") + .old("use a `where` clause in `construct_runtime`") + .new( + "use `frame_system::Config` to set the `Block` type and delete this clause. + It is planned to be removed in December 2023", + ) + .help_links(&["https://github.com/paritytech/substrate/pull/14437"]) + .span(where_section.span) + .build(), ) - }); + }); let res = quote!( #warning @@ -659,7 +662,6 @@ fn decl_all_pallets<'a>( #( #all_pallets_reversed_with_system_first )* ) } - fn decl_pallet_runtime_setup( runtime: &Ident, pallet_declarations: &[Pallet], @@ -667,6 +669,7 @@ fn decl_pallet_runtime_setup( ) -> TokenStream2 { let names = pallet_declarations.iter().map(|d| &d.name).collect::<Vec<_>>(); let name_strings = pallet_declarations.iter().map(|d| d.name.to_string()); + let name_hashes = pallet_declarations.iter().map(|d| two128_str(&d.name.to_string())); let module_names = pallet_declarations.iter().map(|d| d.path.module_name()); let indices = pallet_declarations.iter().map(|pallet| pallet.index as usize); let pallet_structs = pallet_declarations @@ -699,6 +702,7 @@ fn decl_pallet_runtime_setup( pub struct PalletInfo; impl #scrate::traits::PalletInfo for PalletInfo { + fn index<P: 'static>() -> Option<usize> { let type_id = #scrate::__private::sp_std::any::TypeId::of::<P>(); #( @@ -723,6 +727,18 @@ fn decl_pallet_runtime_setup( None } + fn name_hash<P: 'static>() -> Option<[u8; 16]> { + let type_id = #scrate::__private::sp_std::any::TypeId::of::<P>(); + #( + #pallet_attrs + if type_id == #scrate::__private::sp_std::any::TypeId::of::<#names>() { + return Some(#name_hashes) + } + )* + + None + } + fn module_name<P: 'static>() -> Option<&'static str> { let type_id = #scrate::__private::sp_std::any::TypeId::of::<P>(); #( diff --git a/substrate/frame/support/procedural/src/pallet/expand/pallet_struct.rs b/substrate/frame/support/procedural/src/pallet/expand/pallet_struct.rs index e519e34d1dfd9..c2102f0284dbe 100644 --- a/substrate/frame/support/procedural/src/pallet/expand/pallet_struct.rs +++ b/substrate/frame/support/procedural/src/pallet/expand/pallet_struct.rs @@ -246,6 +246,14 @@ pub fn expand_pallet_struct(def: &mut Def) -> proc_macro2::TokenStream { implemented by the runtime") } + fn name_hash() -> [u8; 16] { + < + <T as #frame_system::Config>::PalletInfo as #frame_support::traits::PalletInfo + >::name_hash::<Self>() + .expect("Pallet is part of the runtime because pallet `Config` trait is \ + implemented by the runtime") + } + fn module_name() -> &'static str { < <T as #frame_system::Config>::PalletInfo as #frame_support::traits::PalletInfo diff --git a/substrate/frame/support/procedural/src/pallet/expand/storage.rs b/substrate/frame/support/procedural/src/pallet/expand/storage.rs index c01f0f3926a69..e7f7cf548f0ea 100644 --- a/substrate/frame/support/procedural/src/pallet/expand/storage.rs +++ b/substrate/frame/support/procedural/src/pallet/expand/storage.rs @@ -18,7 +18,10 @@ use crate::{ counter_prefix, pallet::{ - parse::storage::{Metadata, QueryKind, StorageDef, StorageGenerics}, + parse::{ + helper::two128_str, + storage::{Metadata, QueryKind, StorageDef, StorageGenerics}, + }, Def, }, }; @@ -638,6 +641,7 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { Metadata::CountedMap { .. } => { let counter_prefix_struct_ident = counter_prefix_ident(&storage_def.ident); let counter_prefix_struct_const = counter_prefix(&prefix_struct_const); + let storage_prefix_hash = two128_str(&counter_prefix_struct_const); quote::quote_spanned!(storage_def.attr_span => #(#cfg_attrs)* #[doc(hidden)] @@ -656,7 +660,19 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { >::name::<Pallet<#type_use_gen>>() .expect("No name found for the pallet in the runtime! This usually means that the pallet wasn't added to `construct_runtime!`.") } + + fn pallet_prefix_hash() -> [u8; 16] { + < + <T as #frame_system::Config>::PalletInfo + as #frame_support::traits::PalletInfo + >::name_hash::<Pallet<#type_use_gen>>() + .expect("No name_hash found for the pallet in the runtime! This usually means that the pallet wasn't added to `construct_runtime!`.") + } + const STORAGE_PREFIX: &'static str = #counter_prefix_struct_const; + fn storage_prefix_hash() -> [u8; 16] { + #storage_prefix_hash + } } #(#cfg_attrs)* impl<#type_impl_gen> #frame_support::storage::types::CountedStorageMapInstance @@ -670,6 +686,7 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { Metadata::CountedNMap { .. } => { let counter_prefix_struct_ident = counter_prefix_ident(&storage_def.ident); let counter_prefix_struct_const = counter_prefix(&prefix_struct_const); + let storage_prefix_hash = two128_str(&counter_prefix_struct_const); quote::quote_spanned!(storage_def.attr_span => #(#cfg_attrs)* #[doc(hidden)] @@ -688,7 +705,17 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { >::name::<Pallet<#type_use_gen>>() .expect("No name found for the pallet in the runtime! This usually means that the pallet wasn't added to `construct_runtime!`.") } + fn pallet_prefix_hash() -> [u8; 16] { + < + <T as #frame_system::Config>::PalletInfo + as #frame_support::traits::PalletInfo + >::name_hash::<Pallet<#type_use_gen>>() + .expect("No name_hash found for the pallet in the runtime! This usually means that the pallet wasn't added to `construct_runtime!`.") + } const STORAGE_PREFIX: &'static str = #counter_prefix_struct_const; + fn storage_prefix_hash() -> [u8; 16] { + #storage_prefix_hash + } } #(#cfg_attrs)* impl<#type_impl_gen> #frame_support::storage::types::CountedStorageNMapInstance @@ -702,6 +729,7 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { _ => proc_macro2::TokenStream::default(), }; + let storage_prefix_hash = two128_str(&prefix_struct_const); quote::quote_spanned!(storage_def.attr_span => #maybe_counter @@ -722,7 +750,19 @@ pub fn expand_storages(def: &mut Def) -> proc_macro2::TokenStream { >::name::<Pallet<#type_use_gen>>() .expect("No name found for the pallet in the runtime! This usually means that the pallet wasn't added to `construct_runtime!`.") } + + fn pallet_prefix_hash() -> [u8; 16] { + < + <T as #frame_system::Config>::PalletInfo + as #frame_support::traits::PalletInfo + >::name_hash::<Pallet<#type_use_gen>>() + .expect("No name_hash found for the pallet in the runtime! This usually means that the pallet wasn't added to `construct_runtime!`.") + } + const STORAGE_PREFIX: &'static str = #prefix_struct_const; + fn storage_prefix_hash() -> [u8; 16] { + #storage_prefix_hash + } } ) }); diff --git a/substrate/frame/support/procedural/src/pallet/mod.rs b/substrate/frame/support/procedural/src/pallet/mod.rs index 3618711051d7f..42d8272fb23ed 100644 --- a/substrate/frame/support/procedural/src/pallet/mod.rs +++ b/substrate/frame/support/procedural/src/pallet/mod.rs @@ -26,7 +26,7 @@ //! to user defined types. And also crate new types and implement block. mod expand; -mod parse; +pub(crate) mod parse; pub use parse::{composite::keyword::CompositeKeyword, Def}; use syn::spanned::Spanned; diff --git a/substrate/frame/support/procedural/src/pallet/parse/helper.rs b/substrate/frame/support/procedural/src/pallet/parse/helper.rs index bfa19d8ddc39b..446ec203d2ba5 100644 --- a/substrate/frame/support/procedural/src/pallet/parse/helper.rs +++ b/substrate/frame/support/procedural/src/pallet/parse/helper.rs @@ -15,7 +15,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use quote::ToTokens; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; use syn::spanned::Spanned; /// List of additional token to be used for parsing. @@ -610,3 +611,16 @@ pub fn check_pallet_call_return_type(type_: &syn::Type) -> syn::Result<()> { syn::parse2::<Checker>(type_.to_token_stream()).map(|_| ()) } + +pub(crate) fn two128_str(s: &str) -> TokenStream { + bytes_to_array(sp_core_hashing::twox_128(s.as_bytes()).into_iter()) +} + +pub(crate) fn bytes_to_array(bytes: impl IntoIterator<Item = u8>) -> TokenStream { + let bytes = bytes.into_iter(); + + quote!( + [ #( #bytes ),* ] + ) + .into() +} diff --git a/substrate/frame/support/procedural/src/storage_alias.rs b/substrate/frame/support/procedural/src/storage_alias.rs index a3f21806e18b9..4903fd1c129c7 100644 --- a/substrate/frame/support/procedural/src/storage_alias.rs +++ b/substrate/frame/support/procedural/src/storage_alias.rs @@ -17,7 +17,7 @@ //! Implementation of the `storage_alias` attribute macro. -use crate::counter_prefix; +use crate::{counter_prefix, pallet::parse::helper}; use frame_support_procedural_tools::generate_crate_access_2018; use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; @@ -619,6 +619,7 @@ fn generate_storage_instance( let counter_code = is_counted_map.then(|| { let counter_name = Ident::new(&counter_prefix(&name_str), Span::call_site()); let counter_storage_name_str = counter_prefix(&storage_name_str); + let storage_prefix_hash = helper::two128_str(&counter_storage_name_str); quote! { #visibility struct #counter_name< #impl_generics >( @@ -633,6 +634,9 @@ fn generate_storage_instance( } const STORAGE_PREFIX: &'static str = #counter_storage_name_str; + fn storage_prefix_hash() -> [u8; 16] { + #storage_prefix_hash + } } impl<#impl_generics> #crate_::storage::types::CountedStorageMapInstance @@ -643,6 +647,8 @@ fn generate_storage_instance( } }); + let storage_prefix_hash = helper::two128_str(&storage_name_str); + // Implement `StorageInstance` trait. let code = quote! { #[allow(non_camel_case_types)] @@ -658,6 +664,9 @@ fn generate_storage_instance( } const STORAGE_PREFIX: &'static str = #storage_name_str; + fn storage_prefix_hash() -> [u8; 16] { + #storage_prefix_hash + } } #counter_code diff --git a/substrate/frame/support/src/storage/generator/double_map.rs b/substrate/frame/support/src/storage/generator/double_map.rs index 00a3f1bc7c1ce..a4c1f58203e3c 100644 --- a/substrate/frame/support/src/storage/generator/double_map.rs +++ b/substrate/frame/support/src/storage/generator/double_map.rs @@ -33,7 +33,7 @@ use sp_std::prelude::*; /// /// Thus value for (key1, key2) is stored at: /// ```nocompile -/// Twox128(module_prefix) ++ Twox128(storage_prefix) ++ Hasher1(encode(key1)) ++ Hasher2(encode(key2)) +/// Twox128(pallet_prefix) ++ Twox128(storage_prefix) ++ Hasher1(encode(key1)) ++ Hasher2(encode(key2)) /// ``` /// /// # Warning @@ -53,18 +53,15 @@ pub trait StorageDoubleMap<K1: FullEncode, K2: FullEncode, V: FullCodec> { /// Hasher for the second key. type Hasher2: StorageHasher; - /// Module prefix. Used for generating final key. - fn module_prefix() -> &'static [u8]; + /// Pallet prefix. Used for generating final key. + fn pallet_prefix() -> &'static [u8]; /// Storage prefix. Used for generating final key. fn storage_prefix() -> &'static [u8]; - /// The full prefix; just the hash of `module_prefix` concatenated to the hash of + /// The full prefix; just the hash of `pallet_prefix` concatenated to the hash of /// `storage_prefix`. - fn prefix_hash() -> Vec<u8> { - let result = storage_prefix(Self::module_prefix(), Self::storage_prefix()); - result.to_vec() - } + fn prefix_hash() -> [u8; 32]; /// Convert an optional value retrieved from storage to the type queried. fn from_optional_value_to_query(v: Option<V>) -> Self::Query; @@ -77,7 +74,7 @@ pub trait StorageDoubleMap<K1: FullEncode, K2: FullEncode, V: FullCodec> { where KArg1: EncodeLike<K1>, { - let storage_prefix = storage_prefix(Self::module_prefix(), Self::storage_prefix()); + let storage_prefix = storage_prefix(Self::pallet_prefix(), Self::storage_prefix()); let key_hashed = k1.using_encoded(Self::Hasher1::hash); let mut final_key = Vec::with_capacity(storage_prefix.len() + key_hashed.as_ref().len()); @@ -94,7 +91,7 @@ pub trait StorageDoubleMap<K1: FullEncode, K2: FullEncode, V: FullCodec> { KArg1: EncodeLike<K1>, KArg2: EncodeLike<K2>, { - let storage_prefix = storage_prefix(Self::module_prefix(), Self::storage_prefix()); + let storage_prefix = storage_prefix(Self::pallet_prefix(), Self::storage_prefix()); let key1_hashed = k1.using_encoded(Self::Hasher1::hash); let key2_hashed = k2.using_encoded(Self::Hasher2::hash); @@ -334,7 +331,7 @@ where key2: KeyArg2, ) -> Option<V> { let old_key = { - let storage_prefix = storage_prefix(Self::module_prefix(), Self::storage_prefix()); + let storage_prefix = storage_prefix(Self::pallet_prefix(), Self::storage_prefix()); let key1_hashed = key1.using_encoded(OldHasher1::hash); let key2_hashed = key2.using_encoded(OldHasher2::hash); @@ -419,7 +416,7 @@ where } fn iter() -> Self::Iterator { - let prefix = G::prefix_hash(); + let prefix = G::prefix_hash().to_vec(); Self::Iterator { prefix: prefix.clone(), previous_key: prefix, @@ -442,7 +439,7 @@ where } fn iter_keys() -> Self::FullKeyIterator { - let prefix = G::prefix_hash(); + let prefix = G::prefix_hash().to_vec(); Self::FullKeyIterator { prefix: prefix.clone(), previous_key: prefix, @@ -470,7 +467,7 @@ where } fn translate<O: Decode, F: FnMut(K1, K2, O) -> Option<V>>(mut f: F) { - let prefix = G::prefix_hash(); + let prefix = G::prefix_hash().to_vec(); let mut previous_key = prefix.clone(); while let Some(next) = sp_io::storage::next_key(&previous_key).filter(|n| n.starts_with(&prefix)) @@ -561,7 +558,7 @@ mod test_iterators { type DoubleMap = self::frame_system::DoubleMap<Runtime>; // All map iterator - let prefix = DoubleMap::prefix_hash(); + let prefix = DoubleMap::prefix_hash().to_vec(); unhashed::put(&key_before_prefix(prefix.clone()), &1u64); unhashed::put(&key_after_prefix(prefix.clone()), &1u64); @@ -621,7 +618,7 @@ mod test_iterators { assert_eq!(unhashed::get(&key_after_prefix(prefix.clone())), Some(1u64)); // Translate - let prefix = DoubleMap::prefix_hash(); + let prefix = DoubleMap::prefix_hash().to_vec(); unhashed::put(&key_before_prefix(prefix.clone()), &1u64); unhashed::put(&key_after_prefix(prefix.clone()), &1u64); diff --git a/substrate/frame/support/src/storage/generator/map.rs b/substrate/frame/support/src/storage/generator/map.rs index 1d2511e324dc6..b2919bff8d134 100644 --- a/substrate/frame/support/src/storage/generator/map.rs +++ b/substrate/frame/support/src/storage/generator/map.rs @@ -28,7 +28,7 @@ use sp_std::prelude::*; /// /// By default each key value is stored at: /// ```nocompile -/// Twox128(module_prefix) ++ Twox128(storage_prefix) ++ Hasher(encode(key)) +/// Twox128(pallet_prefix) ++ Twox128(storage_prefix) ++ Hasher(encode(key)) /// ``` /// /// # Warning @@ -42,18 +42,15 @@ pub trait StorageMap<K: FullEncode, V: FullCodec> { /// Hasher. Used for generating final key. type Hasher: StorageHasher; - /// Module prefix. Used for generating final key. - fn module_prefix() -> &'static [u8]; + /// Pallet prefix. Used for generating final key. + fn pallet_prefix() -> &'static [u8]; /// Storage prefix. Used for generating final key. fn storage_prefix() -> &'static [u8]; - /// The full prefix; just the hash of `module_prefix` concatenated to the hash of + /// The full prefix; just the hash of `pallet_prefix` concatenated to the hash of /// `storage_prefix`. - fn prefix_hash() -> Vec<u8> { - let result = storage_prefix(Self::module_prefix(), Self::storage_prefix()); - result.to_vec() - } + fn prefix_hash() -> [u8; 32]; /// Convert an optional value retrieved from storage to the type queried. fn from_optional_value_to_query(v: Option<V>) -> Self::Query; @@ -66,7 +63,7 @@ pub trait StorageMap<K: FullEncode, V: FullCodec> { where KeyArg: EncodeLike<K>, { - let storage_prefix = storage_prefix(Self::module_prefix(), Self::storage_prefix()); + let storage_prefix = storage_prefix(Self::pallet_prefix(), Self::storage_prefix()); let key_hashed = key.using_encoded(Self::Hasher::hash); let mut final_key = Vec::with_capacity(storage_prefix.len() + key_hashed.as_ref().len()); @@ -128,7 +125,7 @@ where /// Enumerate all elements in the map. fn iter() -> Self::Iterator { - let prefix = G::prefix_hash(); + let prefix = G::prefix_hash().to_vec(); PrefixIterator { prefix: prefix.clone(), previous_key: prefix, @@ -150,7 +147,7 @@ where /// Enumerate all keys in the map. fn iter_keys() -> Self::KeyIterator { - let prefix = G::prefix_hash(); + let prefix = G::prefix_hash().to_vec(); KeyPrefixIterator { prefix: prefix.clone(), previous_key: prefix, @@ -190,7 +187,7 @@ where previous_key: Option<Vec<u8>>, mut f: F, ) -> Option<Vec<u8>> { - let prefix = G::prefix_hash(); + let prefix = G::prefix_hash().to_vec(); let previous_key = previous_key.unwrap_or_else(|| prefix.clone()); let current_key = @@ -339,7 +336,7 @@ impl<K: FullEncode, V: FullCodec, G: StorageMap<K, V>> storage::StorageMap<K, V> fn migrate_key<OldHasher: StorageHasher, KeyArg: EncodeLike<K>>(key: KeyArg) -> Option<V> { let old_key = { - let storage_prefix = storage_prefix(Self::module_prefix(), Self::storage_prefix()); + let storage_prefix = storage_prefix(Self::pallet_prefix(), Self::storage_prefix()); let key_hashed = key.using_encoded(OldHasher::hash); let mut final_key = @@ -398,7 +395,7 @@ mod test_iterators { type Map = self::frame_system::Map<Runtime>; // All map iterator - let prefix = Map::prefix_hash(); + let prefix = Map::prefix_hash().to_vec(); unhashed::put(&key_before_prefix(prefix.clone()), &1u64); unhashed::put(&key_after_prefix(prefix.clone()), &1u64); @@ -420,7 +417,7 @@ mod test_iterators { assert_eq!(unhashed::get(&key_after_prefix(prefix.clone())), Some(1u64)); // Translate - let prefix = Map::prefix_hash(); + let prefix = Map::prefix_hash().to_vec(); unhashed::put(&key_before_prefix(prefix.clone()), &1u64); unhashed::put(&key_after_prefix(prefix.clone()), &1u64); diff --git a/substrate/frame/support/src/storage/generator/nmap.rs b/substrate/frame/support/src/storage/generator/nmap.rs index 5d3d689aa98a6..4b49ad3eb38d4 100755 --- a/substrate/frame/support/src/storage/generator/nmap.rs +++ b/substrate/frame/support/src/storage/generator/nmap.rs @@ -61,18 +61,15 @@ pub trait StorageNMap<K: KeyGenerator, V: FullCodec> { /// The type that get/take returns. type Query; - /// Module prefix. Used for generating final key. - fn module_prefix() -> &'static [u8]; + /// Pallet prefix. Used for generating final key. + fn pallet_prefix() -> &'static [u8]; /// Storage prefix. Used for generating final key. fn storage_prefix() -> &'static [u8]; - /// The full prefix; just the hash of `module_prefix` concatenated to the hash of + /// The full prefix; just the hash of `pallet_prefix` concatenated to the hash of /// `storage_prefix`. - fn prefix_hash() -> Vec<u8> { - let result = storage_prefix(Self::module_prefix(), Self::storage_prefix()); - result.to_vec() - } + fn prefix_hash() -> [u8; 32]; /// Convert an optional value retrieved from storage to the type queried. fn from_optional_value_to_query(v: Option<V>) -> Self::Query; @@ -85,7 +82,7 @@ pub trait StorageNMap<K: KeyGenerator, V: FullCodec> { where K: HasKeyPrefix<KP>, { - let storage_prefix = storage_prefix(Self::module_prefix(), Self::storage_prefix()); + let storage_prefix = storage_prefix(Self::pallet_prefix(), Self::storage_prefix()); let key_hashed = <K as HasKeyPrefix<KP>>::partial_key(key); let mut final_key = Vec::with_capacity(storage_prefix.len() + key_hashed.len()); @@ -102,7 +99,7 @@ pub trait StorageNMap<K: KeyGenerator, V: FullCodec> { KG: KeyGenerator, KArg: EncodeLikeTuple<KG::KArg> + TupleToEncodedIter, { - let storage_prefix = storage_prefix(Self::module_prefix(), Self::storage_prefix()); + let storage_prefix = storage_prefix(Self::pallet_prefix(), Self::storage_prefix()); let key_hashed = KG::final_key(key); let mut final_key = Vec::with_capacity(storage_prefix.len() + key_hashed.len()); @@ -299,7 +296,7 @@ where KArg: EncodeLikeTuple<K::KArg> + TupleToEncodedIter, { let old_key = { - let storage_prefix = storage_prefix(Self::module_prefix(), Self::storage_prefix()); + let storage_prefix = storage_prefix(Self::pallet_prefix(), Self::storage_prefix()); let key_hashed = K::migrate_key(&key, hash_fns); let mut final_key = Vec::with_capacity(storage_prefix.len() + key_hashed.len()); @@ -386,11 +383,11 @@ impl<K: ReversibleKeyGenerator, V: FullCodec, G: StorageNMap<K, V>> } fn iter() -> Self::Iterator { - Self::iter_from(G::prefix_hash()) + Self::iter_from(G::prefix_hash().to_vec()) } fn iter_from(starting_raw_key: Vec<u8>) -> Self::Iterator { - let prefix = G::prefix_hash(); + let prefix = G::prefix_hash().to_vec(); Self::Iterator { prefix, previous_key: starting_raw_key, @@ -404,11 +401,11 @@ impl<K: ReversibleKeyGenerator, V: FullCodec, G: StorageNMap<K, V>> } fn iter_keys() -> Self::KeyIterator { - Self::iter_keys_from(G::prefix_hash()) + Self::iter_keys_from(G::prefix_hash().to_vec()) } fn iter_keys_from(starting_raw_key: Vec<u8>) -> Self::KeyIterator { - let prefix = G::prefix_hash(); + let prefix = G::prefix_hash().to_vec(); Self::KeyIterator { prefix, previous_key: starting_raw_key, @@ -427,7 +424,7 @@ impl<K: ReversibleKeyGenerator, V: FullCodec, G: StorageNMap<K, V>> } fn translate<O: Decode, F: FnMut(K::Key, O) -> Option<V>>(mut f: F) { - let prefix = G::prefix_hash(); + let prefix = G::prefix_hash().to_vec(); let mut previous_key = prefix.clone(); while let Some(next) = sp_io::storage::next_key(&previous_key).filter(|n| n.starts_with(&prefix)) @@ -537,7 +534,7 @@ mod test_iterators { type NMap = self::frame_system::NMap<Runtime>; // All map iterator - let prefix = NMap::prefix_hash(); + let prefix = NMap::prefix_hash().to_vec(); unhashed::put(&key_before_prefix(prefix.clone()), &1u64); unhashed::put(&key_after_prefix(prefix.clone()), &1u64); @@ -594,7 +591,7 @@ mod test_iterators { assert_eq!(unhashed::get(&key_after_prefix(prefix.clone())), Some(1u64)); // Translate - let prefix = NMap::prefix_hash(); + let prefix = NMap::prefix_hash().to_vec(); unhashed::put(&key_before_prefix(prefix.clone()), &1u64); unhashed::put(&key_after_prefix(prefix.clone()), &1u64); diff --git a/substrate/frame/support/src/storage/generator/value.rs b/substrate/frame/support/src/storage/generator/value.rs index 4ffe40bac53ca..21166b39467bb 100644 --- a/substrate/frame/support/src/storage/generator/value.rs +++ b/substrate/frame/support/src/storage/generator/value.rs @@ -25,14 +25,14 @@ use codec::{Decode, Encode, EncodeLike, FullCodec}; /// /// By default value is stored at: /// ```nocompile -/// Twox128(module_prefix) ++ Twox128(storage_prefix) +/// Twox128(pallet_prefix) ++ Twox128(storage_prefix) /// ``` pub trait StorageValue<T: FullCodec> { /// The type that get/take returns. type Query; - /// Module prefix. Used for generating final key. - fn module_prefix() -> &'static [u8]; + /// Pallet prefix. Used for generating final key. + fn pallet_prefix() -> &'static [u8]; /// Storage prefix. Used for generating final key. fn storage_prefix() -> &'static [u8]; @@ -44,9 +44,7 @@ pub trait StorageValue<T: FullCodec> { fn from_query_to_optional_value(v: Self::Query) -> Option<T>; /// Generate the full key used in top storage. - fn storage_value_final_key() -> [u8; 32] { - crate::storage::storage_prefix(Self::module_prefix(), Self::storage_prefix()) - } + fn storage_value_final_key() -> [u8; 32]; } impl<T: FullCodec, G: StorageValue<T>> storage::StorageValue<T> for G { @@ -97,10 +95,6 @@ impl<T: FullCodec, G: StorageValue<T>> storage::StorageValue<T> for G { } } - fn kill() { - unhashed::kill(&Self::storage_value_final_key()) - } - fn mutate<R, F: FnOnce(&mut G::Query) -> R>(f: F) -> R { Self::try_mutate(|v| Ok::<R, Never>(f(v))).expect("`Never` can not be constructed; qed") } @@ -142,6 +136,10 @@ impl<T: FullCodec, G: StorageValue<T>> storage::StorageValue<T> for G { ret } + fn kill() { + unhashed::kill(&Self::storage_value_final_key()) + } + fn take() -> G::Query { let key = Self::storage_value_final_key(); let value = unhashed::get(&key); diff --git a/substrate/frame/support/src/storage/mod.rs b/substrate/frame/support/src/storage/mod.rs index d52908fa366c6..851b0687bd122 100644 --- a/substrate/frame/support/src/storage/mod.rs +++ b/substrate/frame/support/src/storage/mod.rs @@ -191,7 +191,7 @@ pub trait StorageList<V: FullCodec> { /// Append a single element. /// - /// Should not be called repeatedly; use `append_many` instead. + /// Should not be called repeatedly; use `append_many` instead. /// Worst case linear `O(len)` with `len` being the number if elements in the list. fn append_one<EncodeLikeValue>(item: EncodeLikeValue) where @@ -202,7 +202,7 @@ pub trait StorageList<V: FullCodec> { /// Append many elements. /// - /// Should not be called repeatedly; use `appender` instead. + /// Should not be called repeatedly; use `appender` instead. /// Worst case linear `O(len + items.count())` with `len` beings the number if elements in the /// list. fn append_many<EncodeLikeValue, I>(items: I) @@ -1273,15 +1273,15 @@ impl<T> Iterator for ChildTriePrefixIterator<T> { /// Trait for storage types that store all its value after a unique prefix. pub trait StoragePrefixedContainer { - /// Module prefix. Used for generating final key. - fn module_prefix() -> &'static [u8]; + /// Pallet prefix. Used for generating final key. + fn pallet_prefix() -> &'static [u8]; /// Storage prefix. Used for generating final key. fn storage_prefix() -> &'static [u8]; /// Final full prefix that prefixes all keys. fn final_prefix() -> [u8; 32] { - crate::storage::storage_prefix(Self::module_prefix(), Self::storage_prefix()) + crate::storage::storage_prefix(Self::pallet_prefix(), Self::storage_prefix()) } } @@ -1289,18 +1289,18 @@ pub trait StoragePrefixedContainer { /// /// By default the final prefix is: /// ```nocompile -/// Twox128(module_prefix) ++ Twox128(storage_prefix) +/// Twox128(pallet_prefix) ++ Twox128(storage_prefix) /// ``` pub trait StoragePrefixedMap<Value: FullCodec> { - /// Module prefix. Used for generating final key. - fn module_prefix() -> &'static [u8]; // TODO move to StoragePrefixedContainer + /// Pallet prefix. Used for generating final key. + fn pallet_prefix() -> &'static [u8]; // TODO move to StoragePrefixedContainer /// Storage prefix. Used for generating final key. fn storage_prefix() -> &'static [u8]; /// Final full prefix that prefixes all keys. fn final_prefix() -> [u8; 32] { - crate::storage::storage_prefix(Self::module_prefix(), Self::storage_prefix()) + crate::storage::storage_prefix(Self::pallet_prefix(), Self::storage_prefix()) } /// Remove all values in the overlay and up to `limit` in the backend. @@ -1624,7 +1624,7 @@ mod test { TestExternalities::default().execute_with(|| { struct MyStorage; impl StoragePrefixedMap<u64> for MyStorage { - fn module_prefix() -> &'static [u8] { + fn pallet_prefix() -> &'static [u8] { b"MyModule" } @@ -1701,7 +1701,7 @@ mod test { impl generator::StorageValue<Digest> for Storage { type Query = Digest; - fn module_prefix() -> &'static [u8] { + fn pallet_prefix() -> &'static [u8] { b"MyModule" } @@ -1716,6 +1716,10 @@ mod test { fn from_query_to_optional_value(v: Self::Query) -> Option<Digest> { Some(v) } + + fn storage_value_final_key() -> [u8; 32] { + storage_prefix(Self::pallet_prefix(), Self::storage_prefix()) + } } Storage::append(DigestItem::Other(Vec::new())); @@ -1736,7 +1740,7 @@ mod test { type Query = u64; type Hasher = Twox64Concat; - fn module_prefix() -> &'static [u8] { + fn pallet_prefix() -> &'static [u8] { b"MyModule" } @@ -1744,6 +1748,10 @@ mod test { b"MyStorageMap" } + fn prefix_hash() -> [u8; 32] { + storage_prefix(Self::pallet_prefix(), Self::storage_prefix()) + } + fn from_optional_value_to_query(v: Option<u64>) -> Self::Query { v.unwrap_or_default() } diff --git a/substrate/frame/support/src/storage/types/counted_map.rs b/substrate/frame/support/src/storage/types/counted_map.rs index 5b750a74098b8..50e2c678248c9 100644 --- a/substrate/frame/support/src/storage/types/counted_map.rs +++ b/substrate/frame/support/src/storage/types/counted_map.rs @@ -107,7 +107,7 @@ where /// The prefix used to generate the key of the map. pub fn map_storage_final_prefix() -> Vec<u8> { use crate::storage::generator::StorageMap; - <Self as MapWrapper>::Map::prefix_hash() + <Self as MapWrapper>::Map::prefix_hash().to_vec() } /// Get the storage key used to fetch a value corresponding to a specific key. diff --git a/substrate/frame/support/src/storage/types/counted_nmap.rs b/substrate/frame/support/src/storage/types/counted_nmap.rs index 54f8e57cf242d..5da31c0592254 100644 --- a/substrate/frame/support/src/storage/types/counted_nmap.rs +++ b/substrate/frame/support/src/storage/types/counted_nmap.rs @@ -104,7 +104,7 @@ where /// The prefix used to generate the key of the map. pub fn map_storage_final_prefix() -> Vec<u8> { use crate::storage::generator::StorageNMap; - <Self as MapWrapper>::Map::prefix_hash() + <Self as MapWrapper>::Map::prefix_hash().to_vec() } /// Get the storage key used to fetch a value corresponding to a specific key. diff --git a/substrate/frame/support/src/storage/types/double_map.rs b/substrate/frame/support/src/storage/types/double_map.rs index e787921841032..519ffcbafadee 100644 --- a/substrate/frame/support/src/storage/types/double_map.rs +++ b/substrate/frame/support/src/storage/types/double_map.rs @@ -117,12 +117,17 @@ where type Query = QueryKind::Query; type Hasher1 = Hasher1; type Hasher2 = Hasher2; - fn module_prefix() -> &'static [u8] { + fn pallet_prefix() -> &'static [u8] { Prefix::pallet_prefix().as_bytes() } + fn storage_prefix() -> &'static [u8] { Prefix::STORAGE_PREFIX.as_bytes() } + fn prefix_hash() -> [u8; 32] { + Prefix::prefix_hash() + } + fn from_optional_value_to_query(v: Option<Value>) -> Self::Query { QueryKind::from_optional_value_to_query(v) } @@ -145,8 +150,8 @@ where OnEmpty: Get<QueryKind::Query> + 'static, MaxValues: Get<Option<u32>>, { - fn module_prefix() -> &'static [u8] { - <Self as crate::storage::generator::StorageDoubleMap<Key1, Key2, Value>>::module_prefix() + fn pallet_prefix() -> &'static [u8] { + <Self as crate::storage::generator::StorageDoubleMap<Key1, Key2, Value>>::pallet_prefix() } fn storage_prefix() -> &'static [u8] { <Self as crate::storage::generator::StorageDoubleMap<Key1, Key2, Value>>::storage_prefix() @@ -691,7 +696,7 @@ where { fn storage_info() -> Vec<StorageInfo> { vec![StorageInfo { - pallet_name: Self::module_prefix().to_vec(), + pallet_name: Self::pallet_prefix().to_vec(), storage_name: Self::storage_prefix().to_vec(), prefix: Self::final_prefix().to_vec(), max_values: MaxValues::get(), @@ -722,7 +727,7 @@ where { fn partial_storage_info() -> Vec<StorageInfo> { vec![StorageInfo { - pallet_name: Self::module_prefix().to_vec(), + pallet_name: Self::pallet_prefix().to_vec(), storage_name: Self::storage_prefix().to_vec(), prefix: Self::final_prefix().to_vec(), max_values: MaxValues::get(), diff --git a/substrate/frame/support/src/storage/types/map.rs b/substrate/frame/support/src/storage/types/map.rs index 816b90162f644..7f936a8a35a61 100644 --- a/substrate/frame/support/src/storage/types/map.rs +++ b/substrate/frame/support/src/storage/types/map.rs @@ -83,12 +83,15 @@ where { type Query = QueryKind::Query; type Hasher = Hasher; - fn module_prefix() -> &'static [u8] { + fn pallet_prefix() -> &'static [u8] { Prefix::pallet_prefix().as_bytes() } fn storage_prefix() -> &'static [u8] { Prefix::STORAGE_PREFIX.as_bytes() } + fn prefix_hash() -> [u8; 32] { + Prefix::prefix_hash() + } fn from_optional_value_to_query(v: Option<Value>) -> Self::Query { QueryKind::from_optional_value_to_query(v) } @@ -108,8 +111,8 @@ where OnEmpty: Get<QueryKind::Query> + 'static, MaxValues: Get<Option<u32>>, { - fn module_prefix() -> &'static [u8] { - <Self as crate::storage::generator::StorageMap<Key, Value>>::module_prefix() + fn pallet_prefix() -> &'static [u8] { + <Self as crate::storage::generator::StorageMap<Key, Value>>::pallet_prefix() } fn storage_prefix() -> &'static [u8] { <Self as crate::storage::generator::StorageMap<Key, Value>>::storage_prefix() @@ -469,7 +472,7 @@ where { fn storage_info() -> Vec<StorageInfo> { vec![StorageInfo { - pallet_name: Self::module_prefix().to_vec(), + pallet_name: Self::pallet_prefix().to_vec(), storage_name: Self::storage_prefix().to_vec(), prefix: Self::final_prefix().to_vec(), max_values: MaxValues::get(), @@ -497,7 +500,7 @@ where { fn partial_storage_info() -> Vec<StorageInfo> { vec![StorageInfo { - pallet_name: Self::module_prefix().to_vec(), + pallet_name: Self::pallet_prefix().to_vec(), storage_name: Self::storage_prefix().to_vec(), prefix: Self::final_prefix().to_vec(), max_values: MaxValues::get(), diff --git a/substrate/frame/support/src/storage/types/nmap.rs b/substrate/frame/support/src/storage/types/nmap.rs index e9a4b12dd43a1..406fd42eaf7b3 100755 --- a/substrate/frame/support/src/storage/types/nmap.rs +++ b/substrate/frame/support/src/storage/types/nmap.rs @@ -72,12 +72,15 @@ where MaxValues: Get<Option<u32>>, { type Query = QueryKind::Query; - fn module_prefix() -> &'static [u8] { + fn pallet_prefix() -> &'static [u8] { Prefix::pallet_prefix().as_bytes() } fn storage_prefix() -> &'static [u8] { Prefix::STORAGE_PREFIX.as_bytes() } + fn prefix_hash() -> [u8; 32] { + Prefix::prefix_hash() + } fn from_optional_value_to_query(v: Option<Value>) -> Self::Query { QueryKind::from_optional_value_to_query(v) } @@ -96,8 +99,8 @@ where OnEmpty: Get<QueryKind::Query> + 'static, MaxValues: Get<Option<u32>>, { - fn module_prefix() -> &'static [u8] { - <Self as crate::storage::generator::StorageNMap<Key, Value>>::module_prefix() + fn pallet_prefix() -> &'static [u8] { + <Self as crate::storage::generator::StorageNMap<Key, Value>>::pallet_prefix() } fn storage_prefix() -> &'static [u8] { <Self as crate::storage::generator::StorageNMap<Key, Value>>::storage_prefix() @@ -581,7 +584,7 @@ where { fn storage_info() -> Vec<StorageInfo> { vec![StorageInfo { - pallet_name: Self::module_prefix().to_vec(), + pallet_name: Self::pallet_prefix().to_vec(), storage_name: Self::storage_prefix().to_vec(), prefix: Self::final_prefix().to_vec(), max_values: MaxValues::get(), @@ -607,7 +610,7 @@ where { fn partial_storage_info() -> Vec<StorageInfo> { vec![StorageInfo { - pallet_name: Self::module_prefix().to_vec(), + pallet_name: Self::pallet_prefix().to_vec(), storage_name: Self::storage_prefix().to_vec(), prefix: Self::final_prefix().to_vec(), max_values: MaxValues::get(), diff --git a/substrate/frame/support/src/storage/types/value.rs b/substrate/frame/support/src/storage/types/value.rs index 3c7f24715ac94..3e1f2fe9551d3 100644 --- a/substrate/frame/support/src/storage/types/value.rs +++ b/substrate/frame/support/src/storage/types/value.rs @@ -49,7 +49,7 @@ where OnEmpty: crate::traits::Get<QueryKind::Query> + 'static, { type Query = QueryKind::Query; - fn module_prefix() -> &'static [u8] { + fn pallet_prefix() -> &'static [u8] { Prefix::pallet_prefix().as_bytes() } fn storage_prefix() -> &'static [u8] { @@ -61,6 +61,9 @@ where fn from_query_to_optional_value(v: Self::Query) -> Option<Value> { QueryKind::from_query_to_optional_value(v) } + fn storage_value_final_key() -> [u8; 32] { + Prefix::prefix_hash() + } } impl<Prefix, Value, QueryKind, OnEmpty> StorageValue<Prefix, Value, QueryKind, OnEmpty> @@ -251,7 +254,7 @@ where { fn storage_info() -> Vec<StorageInfo> { vec![StorageInfo { - pallet_name: Self::module_prefix().to_vec(), + pallet_name: Self::pallet_prefix().to_vec(), storage_name: Self::storage_prefix().to_vec(), prefix: Self::hashed_key().to_vec(), max_values: Some(1), @@ -271,7 +274,7 @@ where { fn partial_storage_info() -> Vec<StorageInfo> { vec![StorageInfo { - pallet_name: Self::module_prefix().to_vec(), + pallet_name: Self::pallet_prefix().to_vec(), storage_name: Self::storage_prefix().to_vec(), prefix: Self::hashed_key().to_vec(), max_values: Some(1), diff --git a/substrate/frame/support/src/tests/storage_alias.rs b/substrate/frame/support/src/tests/storage_alias.rs index 05ea1b5f712c6..6fc5cfefdad19 100644 --- a/substrate/frame/support/src/tests/storage_alias.rs +++ b/substrate/frame/support/src/tests/storage_alias.rs @@ -112,7 +112,7 @@ fn verbatim_attribute() { assert_eq!(1, Value::get().unwrap()); // The prefix is the one we declared above. - assert_eq!(&b"Test"[..], Value::module_prefix()); + assert_eq!(&b"Test"[..], Value::pallet_prefix()); }); } @@ -130,7 +130,7 @@ fn pallet_name_attribute() { // The prefix is the pallet name. In this case the pallet name is `System` as declared in // `construct_runtime!`. - assert_eq!(&b"System"[..], Value::<Runtime>::module_prefix()); + assert_eq!(&b"System"[..], Value::<Runtime>::pallet_prefix()); }); } @@ -154,7 +154,7 @@ fn dynamic_attribute() { assert_eq!(1, Value::<Prefix>::get().unwrap()); // The prefix is the one we declared above. - assert_eq!(&b"Hello"[..], Value::<Prefix>::module_prefix()); + assert_eq!(&b"Hello"[..], Value::<Prefix>::pallet_prefix()); }); } @@ -166,13 +166,13 @@ fn storage_alias_guess() { #[crate::storage_alias] pub type Value = StorageValue<Test, u32>; - assert_eq!(&b"Test"[..], Value::module_prefix()); + assert_eq!(&b"Test"[..], Value::pallet_prefix()); // The macro will use the pallet name as prefix. #[crate::storage_alias] pub type PalletValue<T: Config> = StorageValue<Pallet<T>, u32>; - assert_eq!(&b"System"[..], PalletValue::<Runtime>::module_prefix()); + assert_eq!(&b"System"[..], PalletValue::<Runtime>::pallet_prefix()); }); } diff --git a/substrate/frame/support/src/traits/metadata.rs b/substrate/frame/support/src/traits/metadata.rs index 85d8f9a5a74e0..bd29b60091613 100644 --- a/substrate/frame/support/src/traits/metadata.rs +++ b/substrate/frame/support/src/traits/metadata.rs @@ -31,6 +31,8 @@ pub trait PalletInfo { fn index<P: 'static>() -> Option<usize>; /// Convert the given pallet `P` into its name as configured in the runtime. fn name<P: 'static>() -> Option<&'static str>; + /// The two128 hash of name. + fn name_hash<P: 'static>() -> Option<[u8; 16]>; /// Convert the given pallet `P` into its Rust module name as used in `construct_runtime!`. fn module_name<P: 'static>() -> Option<&'static str>; /// Convert the given pallet `P` into its containing crate version. @@ -59,6 +61,8 @@ pub trait PalletInfoAccess { fn index() -> usize; /// Name of the pallet as configured in the runtime. fn name() -> &'static str; + /// Two128 hash of name. + fn name_hash() -> [u8; 16]; /// Name of the Rust module containing the pallet. fn module_name() -> &'static str; /// Version of the crate containing the pallet. @@ -281,6 +285,7 @@ pub trait GetStorageVersion { #[cfg(test)] mod tests { use super::*; + use sp_core::twox_128; struct Pallet1; impl PalletInfoAccess for Pallet1 { @@ -290,6 +295,9 @@ mod tests { fn name() -> &'static str { "Pallet1" } + fn name_hash() -> [u8; 16] { + twox_128(Self::name().as_bytes()) + } fn module_name() -> &'static str { "pallet1" } @@ -305,6 +313,11 @@ mod tests { fn name() -> &'static str { "Pallet2" } + + fn name_hash() -> [u8; 16] { + twox_128(Self::name().as_bytes()) + } + fn module_name() -> &'static str { "pallet2" } diff --git a/substrate/frame/support/src/traits/storage.rs b/substrate/frame/support/src/traits/storage.rs index e0ce1c0fbd317..fe1b9bf13bb02 100644 --- a/substrate/frame/support/src/traits/storage.rs +++ b/substrate/frame/support/src/traits/storage.rs @@ -61,8 +61,35 @@ pub trait StorageInstance { /// Prefix of a pallet to isolate it from other pallets. fn pallet_prefix() -> &'static str; + /// Return the prefix hash of pallet instance. + /// + /// NOTE: This hash must be `twox_128(pallet_prefix())`. + /// Should not impl this function by hand. Only use the default or macro generated impls. + fn pallet_prefix_hash() -> [u8; 16] { + sp_io::hashing::twox_128(Self::pallet_prefix().as_bytes()) + } + /// Prefix given to a storage to isolate from other storages in the pallet. const STORAGE_PREFIX: &'static str; + + /// Return the prefix hash of storage instance. + /// + /// NOTE: This hash must be `twox_128(STORAGE_PREFIX)`. + fn storage_prefix_hash() -> [u8; 16] { + sp_io::hashing::twox_128(Self::STORAGE_PREFIX.as_bytes()) + } + + /// Return the prefix hash of instance. + /// + /// NOTE: This hash must be `twox_128(pallet_prefix())++twox_128(STORAGE_PREFIX)`. + /// Should not impl this function by hand. Only use the default or macro generated impls. + fn prefix_hash() -> [u8; 32] { + let mut final_key = [0u8; 32]; + final_key[..16].copy_from_slice(&Self::pallet_prefix_hash()); + final_key[16..].copy_from_slice(&Self::storage_prefix_hash()); + + final_key + } } /// Metadata about storage from the runtime. diff --git a/substrate/frame/tips/src/tests.rs b/substrate/frame/tips/src/tests.rs index 9cb90c3798018..20d4b2c1a4c11 100644 --- a/substrate/frame/tips/src/tests.rs +++ b/substrate/frame/tips/src/tests.rs @@ -489,7 +489,7 @@ fn test_last_reward_migration() { s.top = data.into_iter().collect(); sp_io::TestExternalities::new(s).execute_with(|| { - let module = pallet_tips::Tips::<Test>::module_prefix(); + let module = pallet_tips::Tips::<Test>::pallet_prefix(); let item = pallet_tips::Tips::<Test>::storage_prefix(); Tips::migrate_retract_tip_for_tip_new(module, item); diff --git a/substrate/frame/tx-pause/src/lib.rs b/substrate/frame/tx-pause/src/lib.rs index f8abf678e5a7e..a3be0f5017270 100644 --- a/substrate/frame/tx-pause/src/lib.rs +++ b/substrate/frame/tx-pause/src/lib.rs @@ -205,7 +205,7 @@ impl<T: Config> Pallet<T> { /// Ensure that this call can be paused. pub fn ensure_can_pause(full_name: &RuntimeCallNameOf<T>) -> Result<(), Error<T>> { // SAFETY: The `TxPause` pallet can never pause itself. - if full_name.0.as_ref() == <Self as PalletInfoAccess>::name().as_bytes().to_vec() { + if full_name.0.as_slice() == <Self as PalletInfoAccess>::name().as_bytes() { return Err(Error::<T>::Unpausable) } From d80171ec229d903ca13673f2bf0d759b7bbb830f Mon Sep 17 00:00:00 2001 From: Svyatoslav Nikolsky <svyatonik@gmail.com> Date: Wed, 4 Oct 2023 01:14:01 +0300 Subject: [PATCH 03/54] Update bridges subtree (#1740) Co-authored-by: Branislav Kontur <bkontur@gmail.com> --- Cargo.lock | 17 + Cargo.toml | 1 + .../runtime-common/src/priority_calculator.rs | 2 +- .../src/refund_relayer_extension.rs | 928 +++++++++++++----- .../chain-bridge-hub-kusama/src/lib.rs | 1 - .../chain-bridge-hub-polkadot/src/lib.rs | 1 - .../chain-bridge-hub-rococo/src/lib.rs | 2 +- .../chain-bridge-hub-wococo/src/lib.rs | 1 - bridges/primitives/chain-kusama/src/lib.rs | 1 - .../chain-polkadot-bulletin/Cargo.toml | 41 + .../chain-polkadot-bulletin/src/lib.rs | 215 ++++ bridges/primitives/chain-polkadot/src/lib.rs | 1 - bridges/primitives/chain-rococo/src/lib.rs | 1 - bridges/primitives/chain-wococo/src/lib.rs | 1 - .../src/justification/verification/mod.rs | 1 + bridges/primitives/runtime/src/chain.rs | 16 +- bridges/primitives/runtime/src/lib.rs | 3 + .../src/bridge_hub_rococo_config.rs | 20 +- .../src/bridge_hub_wococo_config.rs | 20 +- 19 files changed, 1017 insertions(+), 256 deletions(-) create mode 100644 bridges/primitives/chain-polkadot-bulletin/Cargo.toml create mode 100644 bridges/primitives/chain-polkadot-bulletin/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 6d079c53b0f58..b3ffd4b9fb681 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1654,6 +1654,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "bp-polkadot-bulletin" +version = "0.1.0" +dependencies = [ + "bp-header-chain", + "bp-messages", + "bp-polkadot-core", + "bp-runtime", + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", + "sp-api", + "sp-runtime", + "sp-std", +] + [[package]] name = "bp-polkadot-core" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7edc28daf76d1..9c936024e5fad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "bridges/primitives/chain-bridge-hub-wococo", "bridges/primitives/chain-kusama", "bridges/primitives/chain-polkadot", + "bridges/primitives/chain-polkadot-bulletin", "bridges/primitives/chain-rococo", "bridges/primitives/chain-wococo", "bridges/primitives/header-chain", diff --git a/bridges/bin/runtime-common/src/priority_calculator.rs b/bridges/bin/runtime-common/src/priority_calculator.rs index 3d53f9da8c20e..fd10344812517 100644 --- a/bridges/bin/runtime-common/src/priority_calculator.rs +++ b/bridges/bin/runtime-common/src/priority_calculator.rs @@ -38,7 +38,7 @@ where PriorityBoostPerMessage: Get<TransactionPriority>, { // we don't want any boost for transaction with single message => minus one - PriorityBoostPerMessage::get().saturating_mul(messages - 1) + PriorityBoostPerMessage::get().saturating_mul(messages.saturating_sub(1)) } #[cfg(not(feature = "integrity-test"))] diff --git a/bridges/bin/runtime-common/src/refund_relayer_extension.rs b/bridges/bin/runtime-common/src/refund_relayer_extension.rs index f0c2cbf44509b..876c069dc0f77 100644 --- a/bridges/bin/runtime-common/src/refund_relayer_extension.rs +++ b/bridges/bin/runtime-common/src/refund_relayer_extension.rs @@ -24,8 +24,8 @@ use crate::messages_call_ext::{ }; use bp_messages::{LaneId, MessageNonce}; use bp_relayers::{RewardsAccountOwner, RewardsAccountParams}; -use bp_runtime::{Parachain, ParachainIdOf, RangeInclusiveExt, StaticStrProvider}; -use codec::{Decode, Encode}; +use bp_runtime::{Chain, Parachain, ParachainIdOf, RangeInclusiveExt, StaticStrProvider}; +use codec::{Codec, Decode, Encode}; use frame_support::{ dispatch::{CallableCallFor, DispatchInfo, PostDispatchInfo}, traits::IsSubType, @@ -33,7 +33,8 @@ use frame_support::{ CloneNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, }; use pallet_bridge_grandpa::{ - CallSubType as GrandpaCallSubType, SubmitFinalityProofHelper, SubmitFinalityProofInfo, + CallSubType as GrandpaCallSubType, Config as GrandpaConfig, SubmitFinalityProofHelper, + SubmitFinalityProofInfo, }; use pallet_bridge_messages::Config as MessagesConfig; use pallet_bridge_parachains::{ @@ -96,7 +97,7 @@ where /// coming from this lane. pub trait RefundableMessagesLaneId { /// The instance of the bridge messages pallet. - type Instance; + type Instance: 'static; /// The messages lane id. type Id: Get<LaneId>; } @@ -106,6 +107,7 @@ pub struct RefundableMessagesLane<Instance, Id>(PhantomData<(Instance, Id)>); impl<Instance, Id> RefundableMessagesLaneId for RefundableMessagesLane<Instance, Id> where + Instance: 'static, Id: Get<LaneId>, { type Instance = Instance; @@ -165,7 +167,11 @@ pub enum CallInfo { SubmitParachainHeadsInfo, MessagesCallInfo, ), + /// Relay chain finality + message delivery/confirmation calls. + RelayFinalityAndMsgs(SubmitFinalityProofInfo<RelayBlockNumber>, MessagesCallInfo), /// Parachain finality + message delivery/confirmation calls. + /// + /// This variant is used only when bridging with parachain. ParachainFinalityAndMsgs(SubmitParachainHeadsInfo, MessagesCallInfo), /// Standalone message delivery/confirmation call. Msgs(MessagesCallInfo), @@ -184,6 +190,7 @@ impl CallInfo { fn submit_finality_proof_info(&self) -> Option<SubmitFinalityProofInfo<RelayBlockNumber>> { match *self { Self::AllFinalityAndMsgs(info, _, _) => Some(info), + Self::RelayFinalityAndMsgs(info, _) => Some(info), _ => None, } } @@ -201,6 +208,7 @@ impl CallInfo { fn messages_call_info(&self) -> &MessagesCallInfo { match self { Self::AllFinalityAndMsgs(_, _, info) => info, + Self::RelayFinalityAndMsgs(_, info) => info, Self::ParachainFinalityAndMsgs(_, info) => info, Self::Msgs(info) => info, } @@ -209,7 +217,7 @@ impl CallInfo { /// The actions on relayer account that need to be performed because of his actions. #[derive(RuntimeDebug, PartialEq)] -enum RelayerAccountAction<AccountId, Reward> { +pub enum RelayerAccountAction<AccountId, Reward> { /// Do nothing with relayer account. None, /// Reward the relayer. @@ -218,121 +226,60 @@ enum RelayerAccountAction<AccountId, Reward> { Slash(AccountId, RewardsAccountParams), } -/// Signed extension that refunds a relayer for new messages coming from a parachain. -/// -/// Also refunds relayer for successful finality delivery if it comes in batch (`utility.batchAll`) -/// with message delivery transaction. Batch may deliver either both relay chain header and -/// parachain head, or just parachain head. Corresponding headers must be used in messages -/// proof verification. -/// -/// Extension does not refund transaction tip due to security reasons. -#[derive( - DefaultNoBound, - CloneNoBound, - Decode, - Encode, - EqNoBound, - PartialEqNoBound, - RuntimeDebugNoBound, - TypeInfo, -)] -#[scale_info(skip_type_params(Runtime, Para, Msgs, Refund, Priority, Id))] -pub struct RefundBridgedParachainMessages<Runtime, Para, Msgs, Refund, Priority, Id>( - PhantomData<( - // runtime with `frame-utility`, `pallet-bridge-grandpa`, `pallet-bridge-parachains`, - // `pallet-bridge-messages` and `pallet-bridge-relayers` pallets deployed - Runtime, - // implementation of `RefundableParachainId` trait, which specifies the instance of - // the used `pallet-bridge-parachains` pallet and the bridged parachain id - Para, - // implementation of `RefundableMessagesLaneId` trait, which specifies the instance of - // the used `pallet-bridge-messages` pallet and the lane within this pallet - Msgs, - // implementation of the `RefundCalculator` trait, that is used to compute refund that - // we give to relayer for his transaction - Refund, - // getter for per-message `TransactionPriority` boost that we give to message - // delivery transactions - Priority, - // the runtime-unique identifier of this signed extension - Id, - )>, -); - -impl<Runtime, Para, Msgs, Refund, Priority, Id> - RefundBridgedParachainMessages<Runtime, Para, Msgs, Refund, Priority, Id> +/// Everything common among our refund signed extensions. +pub trait RefundSignedExtension: + 'static + Clone + Codec + sp_std::fmt::Debug + Default + Eq + PartialEq + Send + Sync + TypeInfo where - Self: 'static + Send + Sync, - Runtime: UtilityConfig<RuntimeCall = CallOf<Runtime>> - + BoundedBridgeGrandpaConfig<Runtime::BridgesGrandpaPalletInstance> - + ParachainsConfig<Para::Instance> - + MessagesConfig<Msgs::Instance> - + RelayersConfig, - Para: RefundableParachainId, - Msgs: RefundableMessagesLaneId, - Refund: RefundCalculator<Balance = Runtime::Reward>, - Priority: Get<TransactionPriority>, - Id: StaticStrProvider, - CallOf<Runtime>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo> - + IsSubType<CallableCallFor<UtilityPallet<Runtime>, Runtime>> - + GrandpaCallSubType<Runtime, Runtime::BridgesGrandpaPalletInstance> - + ParachainsCallSubType<Runtime, Para::Instance> - + MessagesCallSubType<Runtime, Msgs::Instance>, + <Self::Runtime as GrandpaConfig<Self::GrandpaInstance>>::BridgedChain: + Chain<BlockNumber = RelayBlockNumber>, { - fn expand_call<'a>(&self, call: &'a CallOf<Runtime>) -> Vec<&'a CallOf<Runtime>> { - match call.is_sub_type() { - Some(UtilityCall::<Runtime>::batch_all { ref calls }) if calls.len() <= 3 => - calls.iter().collect(), - Some(_) => vec![], - None => vec![call], - } - } - + /// This chain runtime. + type Runtime: UtilityConfig<RuntimeCall = CallOf<Self::Runtime>> + + GrandpaConfig<Self::GrandpaInstance> + + MessagesConfig<<Self::Msgs as RefundableMessagesLaneId>::Instance> + + RelayersConfig; + /// Grandpa pallet reference. + type GrandpaInstance: 'static; + /// Messages pallet and lane reference. + type Msgs: RefundableMessagesLaneId; + /// Refund amount calculator. + type Refund: RefundCalculator<Balance = <Self::Runtime as RelayersConfig>::Reward>; + /// Priority boost calculator. + type Priority: Get<TransactionPriority>; + /// Signed extension unique identifier. + type Id: StaticStrProvider; + + /// Unpack batch runtime call. + fn expand_call(call: &CallOf<Self::Runtime>) -> Vec<&CallOf<Self::Runtime>>; + + /// Given runtime call, check if it has supported format. Additionally, check if any of + /// (optionally batched) calls are obsolete and we shall reject the transaction. fn parse_and_check_for_obsolete_call( - &self, - call: &CallOf<Runtime>, - ) -> Result<Option<CallInfo>, TransactionValidityError> { - let calls = self.expand_call(call); - let total_calls = calls.len(); - let mut calls = calls.into_iter().map(Self::check_obsolete_call).rev(); + call: &CallOf<Self::Runtime>, + ) -> Result<Option<CallInfo>, TransactionValidityError>; - let msgs_call = calls.next().transpose()?.and_then(|c| c.call_info_for(Msgs::Id::get())); - let para_finality_call = calls - .next() - .transpose()? - .and_then(|c| c.submit_parachain_heads_info_for(Para::Id::get())); - let relay_finality_call = - calls.next().transpose()?.and_then(|c| c.submit_finality_proof_info()); + /// Check if parsed call is already obsolete. + fn check_obsolete_parsed_call( + call: &CallOf<Self::Runtime>, + ) -> Result<&CallOf<Self::Runtime>, TransactionValidityError>; - Ok(match (total_calls, relay_finality_call, para_finality_call, msgs_call) { - (3, Some(relay_finality_call), Some(para_finality_call), Some(msgs_call)) => Some( - CallInfo::AllFinalityAndMsgs(relay_finality_call, para_finality_call, msgs_call), - ), - (2, None, Some(para_finality_call), Some(msgs_call)) => - Some(CallInfo::ParachainFinalityAndMsgs(para_finality_call, msgs_call)), - (1, None, None, Some(msgs_call)) => Some(CallInfo::Msgs(msgs_call)), - _ => None, - }) - } - - fn check_obsolete_call( - call: &CallOf<Runtime>, - ) -> Result<&CallOf<Runtime>, TransactionValidityError> { - call.check_obsolete_submit_finality_proof()?; - call.check_obsolete_submit_parachain_heads()?; - call.check_obsolete_call()?; - Ok(call) - } + /// Called from post-dispatch and shall perform additional checks (apart from relay + /// chain finality and messages transaction finality) of given call result. + fn additional_call_result_check( + relayer: &AccountIdOf<Self::Runtime>, + call_info: &CallInfo, + ) -> bool; /// Given post-dispatch information, analyze the outcome of relayer call and return /// actions that need to be performed on relayer account. fn analyze_call_result( - pre: Option<Option<PreDispatchData<Runtime::AccountId>>>, + pre: Option<Option<PreDispatchData<AccountIdOf<Self::Runtime>>>>, info: &DispatchInfo, post_info: &PostDispatchInfo, len: usize, result: &DispatchResult, - ) -> RelayerAccountAction<AccountIdOf<Runtime>, Runtime::Reward> { + ) -> RelayerAccountAction<AccountIdOf<Self::Runtime>, <Self::Runtime as RelayersConfig>::Reward> + { let mut extra_weight = Weight::zero(); let mut extra_size = 0; @@ -344,15 +291,18 @@ where // now we know that the relayer either needs to be rewarded, or slashed // => let's prepare the correspondent account that pays reward/receives slashed amount - let reward_account_params = RewardsAccountParams::new( - Msgs::Id::get(), - Runtime::BridgedChainId::get(), - if call_info.is_receive_messages_proof_call() { - RewardsAccountOwner::ThisChain - } else { - RewardsAccountOwner::BridgedChain - }, - ); + let reward_account_params = + RewardsAccountParams::new( + <Self::Msgs as RefundableMessagesLaneId>::Id::get(), + <Self::Runtime as MessagesConfig< + <Self::Msgs as RefundableMessagesLaneId>::Instance, + >>::BridgedChainId::get(), + if call_info.is_receive_messages_proof_call() { + RewardsAccountOwner::ThisChain + } else { + RewardsAccountOwner::BridgedChain + }, + ); // prepare return value for the case if the call has failed or it has not caused // expected side effects (e.g. not all messages have been accepted) @@ -376,10 +326,9 @@ where if let Err(e) = result { log::trace!( target: "runtime::bridge", - "{} from parachain {} via {:?}: relayer {:?} has submitted invalid messages transaction: {:?}", - Self::IDENTIFIER, - Para::Id::get(), - Msgs::Id::get(), + "{} via {:?}: relayer {:?} has submitted invalid messages transaction: {:?}", + Self::Id::STR, + <Self::Msgs as RefundableMessagesLaneId>::Id::get(), relayer, e, ); @@ -388,19 +337,18 @@ where // check if relay chain state has been updated if let Some(finality_proof_info) = call_info.submit_finality_proof_info() { - if !SubmitFinalityProofHelper::<Runtime, Runtime::BridgesGrandpaPalletInstance>::was_successful( + if !SubmitFinalityProofHelper::<Self::Runtime, Self::GrandpaInstance>::was_successful( finality_proof_info.block_number, ) { // we only refund relayer if all calls have updated chain state log::trace!( target: "runtime::bridge", - "{} from parachain {} via {:?}: relayer {:?} has submitted invalid relay chain finality proof", - Self::IDENTIFIER, - Para::Id::get(), - Msgs::Id::get(), + "{} via {:?}: relayer {:?} has submitted invalid relay chain finality proof", + Self::Id::STR, + <Self::Msgs as RefundableMessagesLaneId>::Id::get(), relayer, ); - return slash_relayer_if_delivery_result; + return slash_relayer_if_delivery_result } // there's a conflict between how bridge GRANDPA pallet works and a `utility.batchAll` @@ -416,39 +364,25 @@ where extra_size = finality_proof_info.extra_size; } - // check if parachain state has been updated - if let Some(para_proof_info) = call_info.submit_parachain_heads_info() { - if !SubmitParachainHeadsHelper::<Runtime, Para::Instance>::was_successful( - para_proof_info, - ) { - // we only refund relayer if all calls have updated chain state - log::trace!( - target: "runtime::bridge", - "{} from parachain {} via {:?}: relayer {:?} has submitted invalid parachain finality proof", - Self::IDENTIFIER, - Para::Id::get(), - Msgs::Id::get(), - relayer, - ); - return slash_relayer_if_delivery_result - } - } - // Check if the `ReceiveMessagesProof` call delivered at least some of the messages that // it contained. If this happens, we consider the transaction "helpful" and refund it. let msgs_call_info = call_info.messages_call_info(); - if !MessagesCallHelper::<Runtime, Msgs::Instance>::was_successful(msgs_call_info) { + if !MessagesCallHelper::<Self::Runtime, <Self::Msgs as RefundableMessagesLaneId>::Instance>::was_successful(msgs_call_info) { log::trace!( target: "runtime::bridge", - "{} from parachain {} via {:?}: relayer {:?} has submitted invalid messages call", - Self::IDENTIFIER, - Para::Id::get(), - Msgs::Id::get(), + "{} via {:?}: relayer {:?} has submitted invalid messages call", + Self::Id::STR, + <Self::Msgs as RefundableMessagesLaneId>::Id::get(), relayer, ); return slash_relayer_if_delivery_result } + // do additional check + if !Self::additional_call_result_check(&relayer, &call_info) { + return slash_relayer_if_delivery_result + } + // regarding the tip - refund that happens here (at this side of the bridge) isn't the whole // relayer compensation. He'll receive some amount at the other side of the bridge. It shall // (in theory) cover the tip there. Otherwise, if we'll be compensating tip here, some @@ -464,14 +398,14 @@ where // let's also replace the weight of slashing relayer with the weight of rewarding relayer if call_info.is_receive_messages_proof_call() { post_info_weight = post_info_weight.saturating_sub( - <Runtime as RelayersConfig>::WeightInfo::extra_weight_of_successful_receive_messages_proof_call(), + <Self::Runtime as RelayersConfig>::WeightInfo::extra_weight_of_successful_receive_messages_proof_call(), ); } // compute the relayer refund let mut post_info = *post_info; post_info.actual_weight = Some(post_info_weight); - let refund = Refund::compute_refund(info, &post_info, post_info_len, tip); + let refund = Self::Refund::compute_refund(info, &post_info, post_info_len, tip); // we can finally reward relayer RelayerAccountAction::Reward(relayer, reward_account_params, refund) @@ -497,7 +431,11 @@ where let bundled_messages = parsed_call.messages_call_info().bundled_messages().saturating_len(); // a quick check to avoid invalid high-priority transactions - if bundled_messages > Runtime::MaxUnconfirmedMessagesAtInboundLane::get() { + let max_unconfirmed_messages_in_confirmation_tx = <Self::Runtime as MessagesConfig< + <Self::Msgs as RefundableMessagesLaneId>::Instance, + >>::MaxUnconfirmedMessagesAtInboundLane::get( + ); + if bundled_messages > max_unconfirmed_messages_in_confirmation_tx { return None } @@ -505,31 +443,37 @@ where } } -impl<Runtime, Para, Msgs, Refund, Priority, Id> SignedExtension - for RefundBridgedParachainMessages<Runtime, Para, Msgs, Refund, Priority, Id> +/// Adapter that allow implementing `sp_runtime::traits::SignedExtension` for any +/// `RefundSignedExtension`. +#[derive( + DefaultNoBound, + CloneNoBound, + Decode, + Encode, + EqNoBound, + PartialEqNoBound, + RuntimeDebugNoBound, + TypeInfo, +)] +pub struct RefundSignedExtensionAdapter<T: RefundSignedExtension>(T) where - Self: 'static + Send + Sync, - Runtime: UtilityConfig<RuntimeCall = CallOf<Runtime>> - + BoundedBridgeGrandpaConfig<Runtime::BridgesGrandpaPalletInstance> - + ParachainsConfig<Para::Instance> - + MessagesConfig<Msgs::Instance> - + RelayersConfig, - Para: RefundableParachainId, - Msgs: RefundableMessagesLaneId, - Refund: RefundCalculator<Balance = Runtime::Reward>, - Priority: Get<TransactionPriority>, - Id: StaticStrProvider, - CallOf<Runtime>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo> - + IsSubType<CallableCallFor<UtilityPallet<Runtime>, Runtime>> - + GrandpaCallSubType<Runtime, Runtime::BridgesGrandpaPalletInstance> - + ParachainsCallSubType<Runtime, Para::Instance> - + MessagesCallSubType<Runtime, Msgs::Instance>, + <T::Runtime as GrandpaConfig<T::GrandpaInstance>>::BridgedChain: + Chain<BlockNumber = RelayBlockNumber>; + +impl<T: RefundSignedExtension> SignedExtension for RefundSignedExtensionAdapter<T> +where + <T::Runtime as GrandpaConfig<T::GrandpaInstance>>::BridgedChain: + Chain<BlockNumber = RelayBlockNumber>, + CallOf<T::Runtime>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo> + + IsSubType<CallableCallFor<UtilityPallet<T::Runtime>, T::Runtime>> + + GrandpaCallSubType<T::Runtime, T::GrandpaInstance> + + MessagesCallSubType<T::Runtime, <T::Msgs as RefundableMessagesLaneId>::Instance>, { - const IDENTIFIER: &'static str = Id::STR; - type AccountId = Runtime::AccountId; - type Call = CallOf<Runtime>; + const IDENTIFIER: &'static str = T::Id::STR; + type AccountId = AccountIdOf<T::Runtime>; + type Call = CallOf<T::Runtime>; type AdditionalSigned = (); - type Pre = Option<PreDispatchData<Runtime::AccountId>>; + type Pre = Option<PreDispatchData<AccountIdOf<T::Runtime>>>; fn additional_signed(&self) -> Result<(), TransactionValidityError> { Ok(()) @@ -547,34 +491,32 @@ where // we're not calling `validate` from `pre_dispatch` directly because of performance // reasons, so if you're adding some code that may fail here, please check if it needs // to be added to the `pre_dispatch` as well - let parsed_call = self.parse_and_check_for_obsolete_call(call)?; + let parsed_call = T::parse_and_check_for_obsolete_call(call)?; // the following code just plays with transaction priority and never returns an error // we only boost priority of presumably correct message delivery transactions - let bundled_messages = match Self::bundled_messages_for_priority_boost(parsed_call.as_ref()) - { + let bundled_messages = match T::bundled_messages_for_priority_boost(parsed_call.as_ref()) { Some(bundled_messages) => bundled_messages, None => return Ok(Default::default()), }; // we only boost priority if relayer has staked required balance - if !RelayersPallet::<Runtime>::is_registration_active(who) { + if !RelayersPallet::<T::Runtime>::is_registration_active(who) { return Ok(Default::default()) } // compute priority boost let priority_boost = - crate::priority_calculator::compute_priority_boost::<Priority>(bundled_messages); + crate::priority_calculator::compute_priority_boost::<T::Priority>(bundled_messages); let valid_transaction = ValidTransactionBuilder::default().priority(priority_boost); log::trace!( target: "runtime::bridge", - "{} from parachain {} via {:?} has boosted priority of message delivery transaction \ + "{} via {:?} has boosted priority of message delivery transaction \ of relayer {:?}: {} messages -> {} priority", Self::IDENTIFIER, - Para::Id::get(), - Msgs::Id::get(), + <T::Msgs as RefundableMessagesLaneId>::Id::get(), who, bundled_messages, priority_boost, @@ -591,54 +533,294 @@ where _len: usize, ) -> Result<Self::Pre, TransactionValidityError> { // this is a relevant piece of `validate` that we need here (in `pre_dispatch`) - let parsed_call = self.parse_and_check_for_obsolete_call(call)?; + let parsed_call = T::parse_and_check_for_obsolete_call(call)?; Ok(parsed_call.map(|call_info| { log::trace!( target: "runtime::bridge", - "{} from parachain {} via {:?} parsed bridge transaction in pre-dispatch: {:?}", + "{} via {:?} parsed bridge transaction in pre-dispatch: {:?}", Self::IDENTIFIER, - Para::Id::get(), - Msgs::Id::get(), + <T::Msgs as RefundableMessagesLaneId>::Id::get(), call_info, ); PreDispatchData { relayer: who.clone(), call_info } })) } - fn post_dispatch( - pre: Option<Self::Pre>, - info: &DispatchInfoOf<Self::Call>, - post_info: &PostDispatchInfoOf<Self::Call>, - len: usize, - result: &DispatchResult, - ) -> Result<(), TransactionValidityError> { - let call_result = Self::analyze_call_result(pre, info, post_info, len, result); + fn post_dispatch( + pre: Option<Self::Pre>, + info: &DispatchInfoOf<Self::Call>, + post_info: &PostDispatchInfoOf<Self::Call>, + len: usize, + result: &DispatchResult, + ) -> Result<(), TransactionValidityError> { + let call_result = T::analyze_call_result(pre, info, post_info, len, result); + + match call_result { + RelayerAccountAction::None => (), + RelayerAccountAction::Reward(relayer, reward_account, reward) => { + RelayersPallet::<T::Runtime>::register_relayer_reward( + reward_account, + &relayer, + reward, + ); + + log::trace!( + target: "runtime::bridge", + "{} via {:?} has registered reward: {:?} for {:?}", + Self::IDENTIFIER, + <T::Msgs as RefundableMessagesLaneId>::Id::get(), + reward, + relayer, + ); + }, + RelayerAccountAction::Slash(relayer, slash_account) => + RelayersPallet::<T::Runtime>::slash_and_deregister(&relayer, slash_account), + } + + Ok(()) + } +} + +/// Signed extension that refunds a relayer for new messages coming from a parachain. +/// +/// Also refunds relayer for successful finality delivery if it comes in batch (`utility.batchAll`) +/// with message delivery transaction. Batch may deliver either both relay chain header and +/// parachain head, or just parachain head. Corresponding headers must be used in messages +/// proof verification. +/// +/// Extension does not refund transaction tip due to security reasons. +#[derive( + DefaultNoBound, + CloneNoBound, + Decode, + Encode, + EqNoBound, + PartialEqNoBound, + RuntimeDebugNoBound, + TypeInfo, +)] +#[scale_info(skip_type_params(Runtime, Para, Msgs, Refund, Priority, Id))] +pub struct RefundBridgedParachainMessages<Runtime, Para, Msgs, Refund, Priority, Id>( + PhantomData<( + // runtime with `frame-utility`, `pallet-bridge-grandpa`, `pallet-bridge-parachains`, + // `pallet-bridge-messages` and `pallet-bridge-relayers` pallets deployed + Runtime, + // implementation of `RefundableParachainId` trait, which specifies the instance of + // the used `pallet-bridge-parachains` pallet and the bridged parachain id + Para, + // implementation of `RefundableMessagesLaneId` trait, which specifies the instance of + // the used `pallet-bridge-messages` pallet and the lane within this pallet + Msgs, + // implementation of the `RefundCalculator` trait, that is used to compute refund that + // we give to relayer for his transaction + Refund, + // getter for per-message `TransactionPriority` boost that we give to message + // delivery transactions + Priority, + // the runtime-unique identifier of this signed extension + Id, + )>, +); + +impl<Runtime, Para, Msgs, Refund, Priority, Id> RefundSignedExtension + for RefundBridgedParachainMessages<Runtime, Para, Msgs, Refund, Priority, Id> +where + Self: 'static + Send + Sync, + Runtime: UtilityConfig<RuntimeCall = CallOf<Runtime>> + + BoundedBridgeGrandpaConfig<Runtime::BridgesGrandpaPalletInstance> + + ParachainsConfig<Para::Instance> + + MessagesConfig<Msgs::Instance> + + RelayersConfig, + Para: RefundableParachainId, + Msgs: RefundableMessagesLaneId, + Refund: RefundCalculator<Balance = Runtime::Reward>, + Priority: Get<TransactionPriority>, + Id: StaticStrProvider, + CallOf<Runtime>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo> + + IsSubType<CallableCallFor<UtilityPallet<Runtime>, Runtime>> + + GrandpaCallSubType<Runtime, Runtime::BridgesGrandpaPalletInstance> + + ParachainsCallSubType<Runtime, Para::Instance> + + MessagesCallSubType<Runtime, Msgs::Instance>, +{ + type Runtime = Runtime; + type GrandpaInstance = Runtime::BridgesGrandpaPalletInstance; + type Msgs = Msgs; + type Refund = Refund; + type Priority = Priority; + type Id = Id; + + fn expand_call(call: &CallOf<Runtime>) -> Vec<&CallOf<Runtime>> { + match call.is_sub_type() { + Some(UtilityCall::<Runtime>::batch_all { ref calls }) if calls.len() <= 3 => + calls.iter().collect(), + Some(_) => vec![], + None => vec![call], + } + } + + fn parse_and_check_for_obsolete_call( + call: &CallOf<Runtime>, + ) -> Result<Option<CallInfo>, TransactionValidityError> { + let calls = Self::expand_call(call); + let total_calls = calls.len(); + let mut calls = calls.into_iter().map(Self::check_obsolete_parsed_call).rev(); + + let msgs_call = calls.next().transpose()?.and_then(|c| c.call_info_for(Msgs::Id::get())); + let para_finality_call = calls + .next() + .transpose()? + .and_then(|c| c.submit_parachain_heads_info_for(Para::Id::get())); + let relay_finality_call = + calls.next().transpose()?.and_then(|c| c.submit_finality_proof_info()); + + Ok(match (total_calls, relay_finality_call, para_finality_call, msgs_call) { + (3, Some(relay_finality_call), Some(para_finality_call), Some(msgs_call)) => Some( + CallInfo::AllFinalityAndMsgs(relay_finality_call, para_finality_call, msgs_call), + ), + (2, None, Some(para_finality_call), Some(msgs_call)) => + Some(CallInfo::ParachainFinalityAndMsgs(para_finality_call, msgs_call)), + (1, None, None, Some(msgs_call)) => Some(CallInfo::Msgs(msgs_call)), + _ => None, + }) + } + + fn check_obsolete_parsed_call( + call: &CallOf<Runtime>, + ) -> Result<&CallOf<Runtime>, TransactionValidityError> { + call.check_obsolete_submit_finality_proof()?; + call.check_obsolete_submit_parachain_heads()?; + call.check_obsolete_call()?; + Ok(call) + } + + fn additional_call_result_check(relayer: &Runtime::AccountId, call_info: &CallInfo) -> bool { + // check if parachain state has been updated + if let Some(para_proof_info) = call_info.submit_parachain_heads_info() { + if !SubmitParachainHeadsHelper::<Runtime, Para::Instance>::was_successful( + para_proof_info, + ) { + // we only refund relayer if all calls have updated chain state + log::trace!( + target: "runtime::bridge", + "{} from parachain {} via {:?}: relayer {:?} has submitted invalid parachain finality proof", + Id::STR, + Para::Id::get(), + Msgs::Id::get(), + relayer, + ); + return false + } + } + + true + } +} + +/// Signed extension that refunds a relayer for new messages coming from a standalone (GRANDPA) +/// chain. +/// +/// Also refunds relayer for successful finality delivery if it comes in batch (`utility.batchAll`) +/// with message delivery transaction. Batch may deliver either both relay chain header and +/// parachain head, or just parachain head. Corresponding headers must be used in messages +/// proof verification. +/// +/// Extension does not refund transaction tip due to security reasons. +#[derive( + DefaultNoBound, + CloneNoBound, + Decode, + Encode, + EqNoBound, + PartialEqNoBound, + RuntimeDebugNoBound, + TypeInfo, +)] +#[scale_info(skip_type_params(Runtime, GrandpaInstance, Msgs, Refund, Priority, Id))] +pub struct RefundBridgedGrandpaMessages<Runtime, GrandpaInstance, Msgs, Refund, Priority, Id>( + PhantomData<( + // runtime with `frame-utility`, `pallet-bridge-grandpa`, + // `pallet-bridge-messages` and `pallet-bridge-relayers` pallets deployed + Runtime, + // bridge GRANDPA pallet instance, used to track bridged chain state + GrandpaInstance, + // implementation of `RefundableMessagesLaneId` trait, which specifies the instance of + // the used `pallet-bridge-messages` pallet and the lane within this pallet + Msgs, + // implementation of the `RefundCalculator` trait, that is used to compute refund that + // we give to relayer for his transaction + Refund, + // getter for per-message `TransactionPriority` boost that we give to message + // delivery transactions + Priority, + // the runtime-unique identifier of this signed extension + Id, + )>, +); + +impl<Runtime, GrandpaInstance, Msgs, Refund, Priority, Id> RefundSignedExtension + for RefundBridgedGrandpaMessages<Runtime, GrandpaInstance, Msgs, Refund, Priority, Id> +where + Self: 'static + Send + Sync, + Runtime: UtilityConfig<RuntimeCall = CallOf<Runtime>> + + BoundedBridgeGrandpaConfig<GrandpaInstance> + + MessagesConfig<Msgs::Instance> + + RelayersConfig, + GrandpaInstance: 'static, + Msgs: RefundableMessagesLaneId, + Refund: RefundCalculator<Balance = Runtime::Reward>, + Priority: Get<TransactionPriority>, + Id: StaticStrProvider, + CallOf<Runtime>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo> + + IsSubType<CallableCallFor<UtilityPallet<Runtime>, Runtime>> + + GrandpaCallSubType<Runtime, GrandpaInstance> + + MessagesCallSubType<Runtime, Msgs::Instance>, +{ + type Runtime = Runtime; + type GrandpaInstance = GrandpaInstance; + type Msgs = Msgs; + type Refund = Refund; + type Priority = Priority; + type Id = Id; + + fn expand_call(call: &CallOf<Runtime>) -> Vec<&CallOf<Runtime>> { + match call.is_sub_type() { + Some(UtilityCall::<Runtime>::batch_all { ref calls }) if calls.len() <= 2 => + calls.iter().collect(), + Some(_) => vec![], + None => vec![call], + } + } + + fn parse_and_check_for_obsolete_call( + call: &CallOf<Runtime>, + ) -> Result<Option<CallInfo>, TransactionValidityError> { + let calls = Self::expand_call(call); + let total_calls = calls.len(); + let mut calls = calls.into_iter().map(Self::check_obsolete_parsed_call).rev(); - match call_result { - RelayerAccountAction::None => (), - RelayerAccountAction::Reward(relayer, reward_account, reward) => { - RelayersPallet::<Runtime>::register_relayer_reward( - reward_account, - &relayer, - reward, - ); + let msgs_call = calls.next().transpose()?.and_then(|c| c.call_info_for(Msgs::Id::get())); + let relay_finality_call = + calls.next().transpose()?.and_then(|c| c.submit_finality_proof_info()); - log::trace!( - target: "runtime::bridge", - "{} from parachain {} via {:?} has registered reward: {:?} for {:?}", - Self::IDENTIFIER, - Para::Id::get(), - Msgs::Id::get(), - reward, - relayer, - ); - }, - RelayerAccountAction::Slash(relayer, slash_account) => - RelayersPallet::<Runtime>::slash_and_deregister(&relayer, slash_account), - } + Ok(match (total_calls, relay_finality_call, msgs_call) { + (2, Some(relay_finality_call), Some(msgs_call)) => + Some(CallInfo::RelayFinalityAndMsgs(relay_finality_call, msgs_call)), + (1, None, Some(msgs_call)) => Some(CallInfo::Msgs(msgs_call)), + _ => None, + }) + } - Ok(()) + fn check_obsolete_parsed_call( + call: &CallOf<Runtime>, + ) -> Result<&CallOf<Runtime>, TransactionValidityError> { + call.check_obsolete_submit_finality_proof()?; + call.check_obsolete_call()?; + Ok(call) + } + + fn additional_call_result_check(_relayer: &Runtime::AccountId, _call_info: &CallInfo) -> bool { + true } } @@ -655,7 +837,10 @@ mod tests { }, mock::*, }; - use bp_messages::{InboundLaneData, MessageNonce, OutboundLaneData, UnrewardedRelayersState}; + use bp_messages::{ + DeliveredMessages, InboundLaneData, MessageNonce, OutboundLaneData, UnrewardedRelayer, + UnrewardedRelayersState, + }; use bp_parachains::{BestParaHeadHash, ParaInfo}; use bp_polkadot_core::parachains::{ParaHeadsProof, ParaId}; use bp_runtime::HeaderId; @@ -690,7 +875,17 @@ mod tests { } bp_runtime::generate_static_str_provider!(TestExtension); - type TestExtension = RefundBridgedParachainMessages< + + type TestGrandpaExtensionProvider = RefundBridgedGrandpaMessages< + TestRuntime, + (), + RefundableMessagesLane<(), TestLaneId>, + ActualFeeRefund<TestRuntime>, + ConstU64<1>, + StrTestExtension, + >; + type TestGrandpaExtension = RefundSignedExtensionAdapter<TestGrandpaExtensionProvider>; + type TestExtensionProvider = RefundBridgedParachainMessages< TestRuntime, DefaultRefundableParachainId<(), TestParachain>, RefundableMessagesLane<(), TestLaneId>, @@ -698,6 +893,7 @@ mod tests { ConstU64<1>, StrTestExtension, >; + type TestExtension = RefundSignedExtensionAdapter<TestExtensionProvider>; fn initial_balance_of_relayer_account_at_this_chain() -> ThisChainBalance { let test_stake: ThisChainBalance = TestStake::get(); @@ -849,6 +1045,30 @@ mod tests { }) } + fn relay_finality_and_delivery_batch_call( + relay_header_number: RelayBlockNumber, + best_message: MessageNonce, + ) -> RuntimeCall { + RuntimeCall::Utility(UtilityCall::batch_all { + calls: vec![ + submit_relay_header_call(relay_header_number), + message_delivery_call(best_message), + ], + }) + } + + fn relay_finality_and_confirmation_batch_call( + relay_header_number: RelayBlockNumber, + best_message: MessageNonce, + ) -> RuntimeCall { + RuntimeCall::Utility(UtilityCall::batch_all { + calls: vec![ + submit_relay_header_call(relay_header_number), + message_confirmation_call(best_message), + ], + }) + } + fn all_finality_and_delivery_batch_call( relay_header_number: RelayBlockNumber, parachain_head_at_relay_header_number: RelayBlockNumber, @@ -931,6 +1151,50 @@ mod tests { } } + fn relay_finality_pre_dispatch_data() -> PreDispatchData<ThisChainAccountId> { + PreDispatchData { + relayer: relayer_account_at_this_chain(), + call_info: CallInfo::RelayFinalityAndMsgs( + SubmitFinalityProofInfo { + block_number: 200, + extra_weight: Weight::zero(), + extra_size: 0, + }, + MessagesCallInfo::ReceiveMessagesProof(ReceiveMessagesProofInfo { + base: BaseMessagesProofInfo { + lane_id: TEST_LANE_ID, + bundled_range: 101..=200, + best_stored_nonce: 100, + }, + unrewarded_relayers: UnrewardedRelayerOccupation { + free_relayer_slots: MaxUnrewardedRelayerEntriesAtInboundLane::get(), + free_message_slots: MaxUnconfirmedMessagesAtInboundLane::get(), + }, + }), + ), + } + } + + fn relay_finality_confirmation_pre_dispatch_data() -> PreDispatchData<ThisChainAccountId> { + PreDispatchData { + relayer: relayer_account_at_this_chain(), + call_info: CallInfo::RelayFinalityAndMsgs( + SubmitFinalityProofInfo { + block_number: 200, + extra_weight: Weight::zero(), + extra_size: 0, + }, + MessagesCallInfo::ReceiveMessagesDeliveryProof(ReceiveMessagesDeliveryProofInfo( + BaseMessagesProofInfo { + lane_id: TEST_LANE_ID, + bundled_range: 101..=200, + best_stored_nonce: 100, + }, + )), + ), + } + } + fn parachain_finality_pre_dispatch_data() -> PreDispatchData<ThisChainAccountId> { PreDispatchData { relayer: relayer_account_at_this_chain(), @@ -1013,6 +1277,7 @@ mod tests { ) -> PreDispatchData<ThisChainAccountId> { let msg_info = match pre_dispatch_data.call_info { CallInfo::AllFinalityAndMsgs(_, _, ref mut info) => info, + CallInfo::RelayFinalityAndMsgs(_, ref mut info) => info, CallInfo::ParachainFinalityAndMsgs(_, ref mut info) => info, CallInfo::Msgs(ref mut info) => info, }; @@ -1025,7 +1290,14 @@ mod tests { } fn run_validate(call: RuntimeCall) -> TransactionValidity { - let extension: TestExtension = RefundBridgedParachainMessages(PhantomData); + let extension: TestExtension = + RefundSignedExtensionAdapter(RefundBridgedParachainMessages(PhantomData)); + extension.validate(&relayer_account_at_this_chain(), &call, &DispatchInfo::default(), 0) + } + + fn run_grandpa_validate(call: RuntimeCall) -> TransactionValidity { + let extension: TestGrandpaExtension = + RefundSignedExtensionAdapter(RefundBridgedGrandpaMessages(PhantomData)); extension.validate(&relayer_account_at_this_chain(), &call, &DispatchInfo::default(), 0) } @@ -1039,7 +1311,16 @@ mod tests { fn run_pre_dispatch( call: RuntimeCall, ) -> Result<Option<PreDispatchData<ThisChainAccountId>>, TransactionValidityError> { - let extension: TestExtension = RefundBridgedParachainMessages(PhantomData); + let extension: TestExtension = + RefundSignedExtensionAdapter(RefundBridgedParachainMessages(PhantomData)); + extension.pre_dispatch(&relayer_account_at_this_chain(), &call, &DispatchInfo::default(), 0) + } + + fn run_grandpa_pre_dispatch( + call: RuntimeCall, + ) -> Result<Option<PreDispatchData<ThisChainAccountId>>, TransactionValidityError> { + let extension: TestGrandpaExtension = + RefundSignedExtensionAdapter(RefundBridgedGrandpaMessages(PhantomData)); extension.pre_dispatch(&relayer_account_at_this_chain(), &call, &DispatchInfo::default(), 0) } @@ -1674,7 +1955,7 @@ mod tests { pre_dispatch_data: PreDispatchData<ThisChainAccountId>, dispatch_result: DispatchResult, ) -> RelayerAccountAction<ThisChainAccountId, ThisChainBalance> { - TestExtension::analyze_call_result( + TestExtensionProvider::analyze_call_result( Some(Some(pre_dispatch_data)), &dispatch_info(), &post_dispatch_info(), @@ -1737,4 +2018,209 @@ mod tests { ); }); } + + #[test] + fn grandpa_ext_only_parses_valid_batches() { + run_test(|| { + initialize_environment(100, 100, 100); + + // relay + parachain + message delivery calls batch is ignored + assert_eq!( + TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call( + &all_finality_and_delivery_batch_call(200, 200, 200) + ), + Ok(None), + ); + + // relay + parachain + message confirmation calls batch is ignored + assert_eq!( + TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call( + &all_finality_and_confirmation_batch_call(200, 200, 200) + ), + Ok(None), + ); + + // parachain + message delivery call batch is ignored + assert_eq!( + TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call( + ¶chain_finality_and_delivery_batch_call(200, 200) + ), + Ok(None), + ); + + // parachain + message confirmation call batch is ignored + assert_eq!( + TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call( + ¶chain_finality_and_confirmation_batch_call(200, 200) + ), + Ok(None), + ); + + // relay + message delivery call batch is accepted + assert_eq!( + TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call( + &relay_finality_and_delivery_batch_call(200, 200) + ), + Ok(Some(relay_finality_pre_dispatch_data().call_info)), + ); + + // relay + message confirmation call batch is accepted + assert_eq!( + TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call( + &relay_finality_and_confirmation_batch_call(200, 200) + ), + Ok(Some(relay_finality_confirmation_pre_dispatch_data().call_info)), + ); + + // message delivery call batch is accepted + assert_eq!( + TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call( + &message_delivery_call(200) + ), + Ok(Some(delivery_pre_dispatch_data().call_info)), + ); + + // message confirmation call batch is accepted + assert_eq!( + TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call( + &message_confirmation_call(200) + ), + Ok(Some(confirmation_pre_dispatch_data().call_info)), + ); + }); + } + + #[test] + fn grandpa_ext_rejects_batch_with_obsolete_relay_chain_header() { + run_test(|| { + initialize_environment(100, 100, 100); + + assert_eq!( + run_grandpa_pre_dispatch(relay_finality_and_delivery_batch_call(100, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + + assert_eq!( + run_grandpa_validate(relay_finality_and_delivery_batch_call(100, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + }); + } + + #[test] + fn grandpa_ext_rejects_calls_with_obsolete_messages() { + run_test(|| { + initialize_environment(100, 100, 100); + + assert_eq!( + run_grandpa_pre_dispatch(relay_finality_and_delivery_batch_call(200, 100)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + assert_eq!( + run_grandpa_pre_dispatch(relay_finality_and_confirmation_batch_call(200, 100)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + + assert_eq!( + run_grandpa_validate(relay_finality_and_delivery_batch_call(200, 100)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + assert_eq!( + run_grandpa_validate(relay_finality_and_confirmation_batch_call(200, 100)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + + assert_eq!( + run_grandpa_pre_dispatch(message_delivery_call(100)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + assert_eq!( + run_grandpa_pre_dispatch(message_confirmation_call(100)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + + assert_eq!( + run_grandpa_validate(message_delivery_call(100)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + assert_eq!( + run_grandpa_validate(message_confirmation_call(100)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)), + ); + }); + } + + #[test] + fn grandpa_ext_accepts_calls_with_new_messages() { + run_test(|| { + initialize_environment(100, 100, 100); + + assert_eq!( + run_grandpa_pre_dispatch(relay_finality_and_delivery_batch_call(200, 200)), + Ok(Some(relay_finality_pre_dispatch_data()),) + ); + assert_eq!( + run_grandpa_pre_dispatch(relay_finality_and_confirmation_batch_call(200, 200)), + Ok(Some(relay_finality_confirmation_pre_dispatch_data())), + ); + + assert_eq!( + run_grandpa_validate(relay_finality_and_delivery_batch_call(200, 200)), + Ok(Default::default()), + ); + assert_eq!( + run_grandpa_validate(relay_finality_and_confirmation_batch_call(200, 200)), + Ok(Default::default()), + ); + + assert_eq!( + run_grandpa_pre_dispatch(message_delivery_call(200)), + Ok(Some(delivery_pre_dispatch_data())), + ); + assert_eq!( + run_grandpa_pre_dispatch(message_confirmation_call(200)), + Ok(Some(confirmation_pre_dispatch_data())), + ); + + assert_eq!(run_grandpa_validate(message_delivery_call(200)), Ok(Default::default()),); + assert_eq!( + run_grandpa_validate(message_confirmation_call(200)), + Ok(Default::default()), + ); + }); + } + + #[test] + fn does_not_panic_on_boosting_priority_of_empty_message_delivery_transaction() { + run_test(|| { + let best_delivered_message = MaxUnconfirmedMessagesAtInboundLane::get(); + initialize_environment(100, 100, best_delivered_message); + + // register relayer so it gets priority boost + BridgeRelayers::register(RuntimeOrigin::signed(relayer_account_at_this_chain()), 1000) + .unwrap(); + + // allow empty message delivery transactions + let lane_id = TestLaneId::get(); + let in_lane_data = InboundLaneData { + last_confirmed_nonce: 0, + relayers: vec![UnrewardedRelayer { + relayer: relayer_account_at_bridged_chain(), + messages: DeliveredMessages { begin: 1, end: best_delivered_message }, + }] + .into(), + }; + pallet_bridge_messages::InboundLanes::<TestRuntime>::insert(lane_id, in_lane_data); + + // now check that the priority of empty tx is the same as priority of 1-message tx + let priority_of_zero_messages_delivery = + run_validate(message_delivery_call(best_delivered_message)).unwrap().priority; + let priority_of_one_messages_delivery = + run_validate(message_delivery_call(best_delivered_message + 1)) + .unwrap() + .priority; + + assert_eq!(priority_of_zero_messages_delivery, priority_of_one_messages_delivery); + }); + } } diff --git a/bridges/primitives/chain-bridge-hub-kusama/src/lib.rs b/bridges/primitives/chain-bridge-hub-kusama/src/lib.rs index 3a919648df47f..66e0dad05895c 100644 --- a/bridges/primitives/chain-bridge-hub-kusama/src/lib.rs +++ b/bridges/primitives/chain-bridge-hub-kusama/src/lib.rs @@ -29,7 +29,6 @@ use frame_support::{ sp_runtime::{MultiAddress, MultiSigner}, }; use sp_runtime::RuntimeDebug; -use sp_std::prelude::Vec; /// BridgeHubKusama parachain. #[derive(RuntimeDebug)] diff --git a/bridges/primitives/chain-bridge-hub-polkadot/src/lib.rs b/bridges/primitives/chain-bridge-hub-polkadot/src/lib.rs index bf8d8e07c3a61..c3661c1adcada 100644 --- a/bridges/primitives/chain-bridge-hub-polkadot/src/lib.rs +++ b/bridges/primitives/chain-bridge-hub-polkadot/src/lib.rs @@ -26,7 +26,6 @@ use bp_runtime::{ }; use frame_support::dispatch::DispatchClass; use sp_runtime::RuntimeDebug; -use sp_std::prelude::Vec; /// BridgeHubPolkadot parachain. #[derive(RuntimeDebug)] diff --git a/bridges/primitives/chain-bridge-hub-rococo/src/lib.rs b/bridges/primitives/chain-bridge-hub-rococo/src/lib.rs index b726c62ac42b3..a50bda23ac8d3 100644 --- a/bridges/primitives/chain-bridge-hub-rococo/src/lib.rs +++ b/bridges/primitives/chain-bridge-hub-rococo/src/lib.rs @@ -26,7 +26,7 @@ use bp_runtime::{ }; use frame_support::dispatch::DispatchClass; use sp_runtime::{MultiAddress, MultiSigner, RuntimeDebug}; -use sp_std::prelude::Vec; + /// BridgeHubRococo parachain. #[derive(RuntimeDebug)] pub struct BridgeHubRococo; diff --git a/bridges/primitives/chain-bridge-hub-wococo/src/lib.rs b/bridges/primitives/chain-bridge-hub-wococo/src/lib.rs index 5e4758645d9ea..ce4600f5ff35d 100644 --- a/bridges/primitives/chain-bridge-hub-wococo/src/lib.rs +++ b/bridges/primitives/chain-bridge-hub-wococo/src/lib.rs @@ -26,7 +26,6 @@ use bp_runtime::{ }; use frame_support::dispatch::DispatchClass; use sp_runtime::RuntimeDebug; -use sp_std::prelude::Vec; /// BridgeHubWococo parachain. #[derive(RuntimeDebug)] diff --git a/bridges/primitives/chain-kusama/src/lib.rs b/bridges/primitives/chain-kusama/src/lib.rs index 8c3fbd9c203e5..d5748aa132cea 100644 --- a/bridges/primitives/chain-kusama/src/lib.rs +++ b/bridges/primitives/chain-kusama/src/lib.rs @@ -23,7 +23,6 @@ pub use bp_polkadot_core::*; use bp_header_chain::ChainWithGrandpa; use bp_runtime::{decl_bridge_finality_runtime_apis, Chain}; use frame_support::weights::Weight; -use sp_std::prelude::Vec; /// Kusama Chain pub struct Kusama; diff --git a/bridges/primitives/chain-polkadot-bulletin/Cargo.toml b/bridges/primitives/chain-polkadot-bulletin/Cargo.toml new file mode 100644 index 0000000000000..4311aec47276e --- /dev/null +++ b/bridges/primitives/chain-polkadot-bulletin/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "bp-polkadot-bulletin" +description = "Primitives of Polkadot Bulletin chain runtime." +version = "0.1.0" +authors = ["Parity Technologies <admin@parity.io>"] +edition = "2021" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.1.5", default-features = false, features = ["derive"] } +scale-info = { version = "2.9.0", default-features = false, features = ["derive"] } + +# Bridge Dependencies + +bp-header-chain = { path = "../header-chain", default-features = false } +bp-messages = { path = "../messages", default-features = false } +bp-polkadot-core = { path = "../polkadot-core", default-features = false } +bp-runtime = { path = "../runtime", default-features = false } + +# Substrate Based Dependencies + +frame-support = { path = "../../../substrate/frame/support", default-features = false } +frame-system = { path = "../../../substrate/frame/system", default-features = false } +sp-api = { path = "../../../substrate/primitives/api", default-features = false } +sp-runtime = { path = "../../../substrate/primitives/runtime", default-features = false } +sp-std = { path = "../../../substrate/primitives/std", default-features = false } + +[features] +default = [ "std" ] +std = [ + "bp-header-chain/std", + "bp-messages/std", + "bp-polkadot-core/std", + "bp-runtime/std", + "codec/std", + "frame-support/std", + "frame-system/std", + "sp-api/std", + "sp-runtime/std", + "sp-std/std", +] diff --git a/bridges/primitives/chain-polkadot-bulletin/src/lib.rs b/bridges/primitives/chain-polkadot-bulletin/src/lib.rs new file mode 100644 index 0000000000000..fcc6e90eb1b29 --- /dev/null +++ b/bridges/primitives/chain-polkadot-bulletin/src/lib.rs @@ -0,0 +1,215 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>. + +//! Polkadot Bulletin Chain primitives. + +#![warn(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +use bp_header_chain::ChainWithGrandpa; +use bp_messages::MessageNonce; +use bp_runtime::{ + decl_bridge_finality_runtime_apis, decl_bridge_messages_runtime_apis, + extensions::{ + CheckEra, CheckGenesis, CheckNonZeroSender, CheckNonce, CheckSpecVersion, CheckTxVersion, + CheckWeight, GenericSignedExtension, GenericSignedExtensionSchema, + }, + Chain, TransactionEra, +}; +use codec::{Decode, Encode}; +use frame_support::{ + dispatch::DispatchClass, + parameter_types, + weights::{constants::WEIGHT_REF_TIME_PER_SECOND, Weight}, +}; +use frame_system::limits; +use scale_info::TypeInfo; +use sp_runtime::{traits::DispatchInfoOf, transaction_validity::TransactionValidityError, Perbill}; + +// This chain reuses most of Polkadot primitives. +pub use bp_polkadot_core::{ + AccountAddress, AccountId, Balance, Block, BlockNumber, Hash, Hasher, Header, Nonce, Signature, + SignedBlock, UncheckedExtrinsic, AVERAGE_HEADER_SIZE_IN_JUSTIFICATION, + EXTRA_STORAGE_PROOF_SIZE, MAX_HEADER_SIZE, REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY, +}; + +/// Maximal number of GRANDPA authorities at Polkadot Bulletin chain. +pub const MAX_AUTHORITIES_COUNT: u32 = 100; + +/// Name of the With-Polkadot Bulletin chain GRANDPA pallet instance that is deployed at bridged +/// chains. +pub const WITH_POLKADOT_BULLETIN_GRANDPA_PALLET_NAME: &str = "BridgePolkadotBulletinGrandpa"; +/// Name of the With-Polkadot Bulletin chain messages pallet instance that is deployed at bridged +/// chains. +pub const WITH_POLKADOT_BULLETIN_MESSAGES_PALLET_NAME: &str = "BridgePolkadotBulletinMessages"; + +// There are fewer system operations on this chain (e.g. staking, governance, etc.). Use a higher +// percentage of the block for data storage. +const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(90); + +// Re following constants - we are using the same values at Cumulus parachains. They are limited +// by the maximal transaction weight/size. Since block limits at Bulletin Chain are larger than +// at the Cumulus Bridgeg Hubs, we could reuse the same values. + +/// Maximal number of unrewarded relayer entries at inbound lane for Cumulus-based parachains. +pub const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 1024; + +/// Maximal number of unconfirmed messages at inbound lane for Cumulus-based parachains. +pub const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 4096; + +/// This signed extension is used to ensure that the chain transactions are signed by proper +pub type ValidateSigned = GenericSignedExtensionSchema<(), ()>; + +/// Signed extension schema, used by Polkadot Bulletin. +pub type SignedExtensionSchema = GenericSignedExtension<( + ( + CheckNonZeroSender, + CheckSpecVersion, + CheckTxVersion, + CheckGenesis<Hash>, + CheckEra<Hash>, + CheckNonce<Nonce>, + CheckWeight, + ), + ValidateSigned, +)>; + +/// Signed extension, used by Polkadot Bulletin. +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] +pub struct SignedExtension(SignedExtensionSchema); + +impl sp_runtime::traits::SignedExtension for SignedExtension { + const IDENTIFIER: &'static str = "Not needed."; + type AccountId = (); + type Call = (); + type AdditionalSigned = + <SignedExtensionSchema as sp_runtime::traits::SignedExtension>::AdditionalSigned; + type Pre = (); + + fn additional_signed(&self) -> Result<Self::AdditionalSigned, TransactionValidityError> { + self.0.additional_signed() + } + + fn pre_dispatch( + self, + _who: &Self::AccountId, + _call: &Self::Call, + _info: &DispatchInfoOf<Self::Call>, + _len: usize, + ) -> Result<Self::Pre, TransactionValidityError> { + Ok(()) + } +} + +impl SignedExtension { + /// Create signed extension from its components. + pub fn from_params( + spec_version: u32, + transaction_version: u32, + era: TransactionEra<BlockNumber, Hash>, + genesis_hash: Hash, + nonce: Nonce, + ) -> Self { + Self(GenericSignedExtension::new( + ( + ( + (), // non-zero sender + (), // spec version + (), // tx version + (), // genesis + era.frame_era(), // era + nonce.into(), // nonce (compact encoding) + (), // Check weight + ), + (), + ), + Some(( + ( + (), + spec_version, + transaction_version, + genesis_hash, + era.signed_payload(genesis_hash), + (), + (), + ), + (), + )), + )) + } + + /// Return transaction nonce. + pub fn nonce(&self) -> Nonce { + let common_payload = self.0.payload.0; + common_payload.5 .0 + } +} + +parameter_types! { + /// We allow for 2 seconds of compute with a 6 second average block time. + pub BlockWeights: limits::BlockWeights = limits::BlockWeights::with_sensible_defaults( + Weight::from_parts(2u64 * WEIGHT_REF_TIME_PER_SECOND, u64::MAX), + NORMAL_DISPATCH_RATIO, + ); + // Note: Max transaction size is 8 MB. Set max block size to 10 MB to facilitate data storage. + // This is double the "normal" Relay Chain block length limit. + /// Maximal block length at Polkadot Bulletin chain. + pub BlockLength: limits::BlockLength = limits::BlockLength::max_with_normal_ratio( + 10 * 1024 * 1024, + NORMAL_DISPATCH_RATIO, + ); +} + +/// Polkadot Bulletin Chain declaration. +pub struct PolkadotBulletin; + +impl Chain for PolkadotBulletin { + type BlockNumber = BlockNumber; + type Hash = Hash; + type Hasher = Hasher; + type Header = Header; + + type AccountId = AccountId; + // The Bulletin Chain is a permissioned blockchain without any balances. Our `Chain` trait + // requires balance type, which is then used by various bridge infrastructure code. However + // this code is optional and we are not planning to use it in our bridge. + type Balance = Balance; + type Nonce = Nonce; + type Signature = Signature; + + fn max_extrinsic_size() -> u32 { + *BlockLength::get().max.get(DispatchClass::Normal) + } + + fn max_extrinsic_weight() -> Weight { + BlockWeights::get() + .get(DispatchClass::Normal) + .max_extrinsic + .unwrap_or(Weight::MAX) + } +} + +impl ChainWithGrandpa for PolkadotBulletin { + const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str = WITH_POLKADOT_BULLETIN_GRANDPA_PALLET_NAME; + const MAX_AUTHORITIES_COUNT: u32 = MAX_AUTHORITIES_COUNT; + const REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY: u32 = + REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY; + const MAX_HEADER_SIZE: u32 = MAX_HEADER_SIZE; + const AVERAGE_HEADER_SIZE_IN_JUSTIFICATION: u32 = AVERAGE_HEADER_SIZE_IN_JUSTIFICATION; +} + +decl_bridge_finality_runtime_apis!(polkadot_bulletin, grandpa); +decl_bridge_messages_runtime_apis!(polkadot_bulletin); diff --git a/bridges/primitives/chain-polkadot/src/lib.rs b/bridges/primitives/chain-polkadot/src/lib.rs index d1d6f74543121..61c8ca927d807 100644 --- a/bridges/primitives/chain-polkadot/src/lib.rs +++ b/bridges/primitives/chain-polkadot/src/lib.rs @@ -23,7 +23,6 @@ pub use bp_polkadot_core::*; use bp_header_chain::ChainWithGrandpa; use bp_runtime::{decl_bridge_finality_runtime_apis, extensions::PrevalidateAttests, Chain}; use frame_support::weights::Weight; -use sp_std::prelude::Vec; /// Polkadot Chain pub struct Polkadot; diff --git a/bridges/primitives/chain-rococo/src/lib.rs b/bridges/primitives/chain-rococo/src/lib.rs index 1589d14ea5143..5436ad846468c 100644 --- a/bridges/primitives/chain-rococo/src/lib.rs +++ b/bridges/primitives/chain-rococo/src/lib.rs @@ -23,7 +23,6 @@ pub use bp_polkadot_core::*; use bp_header_chain::ChainWithGrandpa; use bp_runtime::{decl_bridge_finality_runtime_apis, Chain}; use frame_support::{parameter_types, weights::Weight}; -use sp_std::prelude::Vec; /// Rococo Chain pub struct Rococo; diff --git a/bridges/primitives/chain-wococo/src/lib.rs b/bridges/primitives/chain-wococo/src/lib.rs index 5b5bde8269044..b1df65630beff 100644 --- a/bridges/primitives/chain-wococo/src/lib.rs +++ b/bridges/primitives/chain-wococo/src/lib.rs @@ -26,7 +26,6 @@ pub use bp_rococo::{ use bp_header_chain::ChainWithGrandpa; use bp_runtime::{decl_bridge_finality_runtime_apis, Chain}; use frame_support::weights::Weight; -use sp_std::prelude::Vec; /// Wococo Chain pub struct Wococo; diff --git a/bridges/primitives/header-chain/src/justification/verification/mod.rs b/bridges/primitives/header-chain/src/justification/verification/mod.rs index bb8aaadf327ec..a66fc1e0d91d1 100644 --- a/bridges/primitives/header-chain/src/justification/verification/mod.rs +++ b/bridges/primitives/header-chain/src/justification/verification/mod.rs @@ -143,6 +143,7 @@ pub enum PrecommitError { } /// The context needed for validating GRANDPA finality proofs. +#[derive(RuntimeDebug)] pub struct JustificationVerificationContext { /// The authority set used to verify the justification. pub voter_set: VoterSet<AuthorityId>, diff --git a/bridges/primitives/runtime/src/chain.rs b/bridges/primitives/runtime/src/chain.rs index 5caaebd42babc..e1809e145248f 100644 --- a/bridges/primitives/runtime/src/chain.rs +++ b/bridges/primitives/runtime/src/chain.rs @@ -311,7 +311,7 @@ macro_rules! decl_bridge_finality_runtime_apis { $( /// Returns the justifications accepted in the current block. fn [<synced_headers_ $consensus:lower _info>]( - ) -> Vec<$justification_type>; + ) -> sp_std::vec::Vec<$justification_type>; )? } } @@ -360,10 +360,10 @@ macro_rules! decl_bridge_messages_runtime_apis { /// If some (or all) messages are missing from the storage, they'll also will /// be missing from the resulting vector. The vector is ordered by the nonce. fn message_details( - lane: LaneId, - begin: MessageNonce, - end: MessageNonce, - ) -> Vec<OutboundMessageDetails>; + lane: bp_messages::LaneId, + begin: bp_messages::MessageNonce, + end: bp_messages::MessageNonce, + ) -> sp_std::vec::Vec<bp_messages::OutboundMessageDetails>; } /// Inbound message lane API for messages sent by this chain. @@ -376,9 +376,9 @@ macro_rules! decl_bridge_messages_runtime_apis { pub trait [<From $chain:camel InboundLaneApi>] { /// Return details of given inbound messages. fn message_details( - lane: LaneId, - messages: Vec<(MessagePayload, OutboundMessageDetails)>, - ) -> Vec<InboundMessageDetails>; + lane: bp_messages::LaneId, + messages: sp_std::vec::Vec<(bp_messages::MessagePayload, bp_messages::OutboundMessageDetails)>, + ) -> sp_std::vec::Vec<bp_messages::InboundMessageDetails>; } } } diff --git a/bridges/primitives/runtime/src/lib.rs b/bridges/primitives/runtime/src/lib.rs index ece782e352bc2..7f4a1a030b145 100644 --- a/bridges/primitives/runtime/src/lib.rs +++ b/bridges/primitives/runtime/src/lib.rs @@ -74,6 +74,9 @@ pub const MILLAU_CHAIN_ID: ChainId = *b"mlau"; /// Polkadot chain id. pub const POLKADOT_CHAIN_ID: ChainId = *b"pdot"; +/// Polkadot Bulletin chain id. +pub const POLKADOT_BULLETIN_CHAIN_ID: ChainId = *b"pdbc"; + /// Kusama chain id. pub const KUSAMA_CHAIN_ID: ChainId = *b"ksma"; diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_rococo_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_rococo_config.rs index bc8f97ad97c1c..f59c9e238f5f2 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_rococo_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_rococo_config.rs @@ -29,8 +29,8 @@ use bridge_runtime_common::{ }, messages_xcm_extension::{SenderAndLane, XcmBlobHauler, XcmBlobHaulerAdapter}, refund_relayer_extension::{ - ActualFeeRefund, RefundBridgedParachainMessages, RefundableMessagesLane, - RefundableParachain, + ActualFeeRefund, RefundBridgedParachainMessages, RefundSignedExtensionAdapter, + RefundableMessagesLane, RefundableParachain, }, }; use frame_support::{parameter_types, traits::PalletInfoAccess}; @@ -136,13 +136,15 @@ impl ThisChainWithMessages for BridgeHubRococo { } /// Signed extension that refunds relayers that are delivering messages from the Wococo parachain. -pub type BridgeRefundBridgeHubWococoMessages = RefundBridgedParachainMessages< - Runtime, - RefundableParachain<BridgeParachainWococoInstance, bp_bridge_hub_wococo::BridgeHubWococo>, - RefundableMessagesLane<WithBridgeHubWococoMessagesInstance, BridgeHubWococoMessagesLane>, - ActualFeeRefund<Runtime>, - PriorityBoostPerMessage, - StrBridgeRefundBridgeHubWococoMessages, +pub type BridgeRefundBridgeHubWococoMessages = RefundSignedExtensionAdapter< + RefundBridgedParachainMessages< + Runtime, + RefundableParachain<BridgeParachainWococoInstance, bp_bridge_hub_wococo::BridgeHubWococo>, + RefundableMessagesLane<WithBridgeHubWococoMessagesInstance, BridgeHubWococoMessagesLane>, + ActualFeeRefund<Runtime>, + PriorityBoostPerMessage, + StrBridgeRefundBridgeHubWococoMessages, + >, >; bp_runtime::generate_static_str_provider!(BridgeRefundBridgeHubWococoMessages); diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_wococo_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_wococo_config.rs index 5178b75c30390..a0b16bace51d0 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_wococo_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_hub_wococo_config.rs @@ -29,8 +29,8 @@ use bridge_runtime_common::{ }, messages_xcm_extension::{SenderAndLane, XcmBlobHauler, XcmBlobHaulerAdapter}, refund_relayer_extension::{ - ActualFeeRefund, RefundBridgedParachainMessages, RefundableMessagesLane, - RefundableParachain, + ActualFeeRefund, RefundBridgedParachainMessages, RefundSignedExtensionAdapter, + RefundableMessagesLane, RefundableParachain, }, }; use frame_support::{parameter_types, traits::PalletInfoAccess}; @@ -136,13 +136,15 @@ impl ThisChainWithMessages for BridgeHubWococo { } /// Signed extension that refunds relayers that are delivering messages from the Rococo parachain. -pub type BridgeRefundBridgeHubRococoMessages = RefundBridgedParachainMessages< - Runtime, - RefundableParachain<BridgeParachainRococoInstance, bp_bridge_hub_rococo::BridgeHubRococo>, - RefundableMessagesLane<WithBridgeHubRococoMessagesInstance, BridgeHubRococoMessagesLane>, - ActualFeeRefund<Runtime>, - PriorityBoostPerMessage, - StrBridgeRefundBridgeHubRococoMessages, +pub type BridgeRefundBridgeHubRococoMessages = RefundSignedExtensionAdapter< + RefundBridgedParachainMessages< + Runtime, + RefundableParachain<BridgeParachainRococoInstance, bp_bridge_hub_rococo::BridgeHubRococo>, + RefundableMessagesLane<WithBridgeHubRococoMessagesInstance, BridgeHubRococoMessagesLane>, + ActualFeeRefund<Runtime>, + PriorityBoostPerMessage, + StrBridgeRefundBridgeHubRococoMessages, + >, >; bp_runtime::generate_static_str_provider!(BridgeRefundBridgeHubRococoMessages); From cd076d7044029c1728881e5f86766d1c9c85f375 Mon Sep 17 00:00:00 2001 From: Javier Bullrich <javier@bullrich.dev> Date: Wed, 4 Oct 2023 09:07:20 +0200 Subject: [PATCH 04/54] Upgraded review-bot to version 2.0.1 (#1784) ## [Updated review bot version](https://github.com/paritytech/polkadot-sdk/commit/677610ba330a8e03f24b46889787e0475f354954) updated version to version `2.0.1` which contains https://github.com/paritytech/review-bot/pull/90, a fix for the team members not being fetch in its totality. ## [Updated review-bot.yml minApprovals convention](https://github.com/paritytech/polkadot-sdk/commit/b1446832ddd14f55b2720a65b0ff86b139999419) Renamed `min_approvals` to `minApprovals`. A breaking change in https://github.com/paritytech/review-bot/pull/86 which was done to standarize all the cases (so now everything is camelCase) --- .github/review-bot.yml | 23 +++++++++++------------ .github/workflows/review-bot.yml | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/review-bot.yml b/.github/review-bot.yml index c9eadd6e58ba1..581e33762608a 100644 --- a/.github/review-bot.yml +++ b/.github/review-bot.yml @@ -10,7 +10,7 @@ rules: - ^\.cargo/.* exclude: - ^./gitlab/pipeline/zombienet.* - min_approvals: 2 + minApprovals: 2 type: basic teams: - ci @@ -27,7 +27,7 @@ rules: exclude: - ^polkadot/runtime\/(kusama|polkadot)\/src\/weights\/.+\.rs$ - ^substrate\/frame\/.+\.md$ - min_approvals: 1 + minApprovals: 1 allowedToSkipRule: teams: - core-devs @@ -54,7 +54,7 @@ rules: - ^\.gitlab/.* - ^\.config/nextest.toml - ^\.cargo/.* - min_approvals: 2 + minApprovals: 2 type: basic teams: - core-devs @@ -70,10 +70,10 @@ rules: - ^cumulus/parachains/common/src/[^/]+\.rs$ type: and-distinct reviewers: - - min_approvals: 1 + - minApprovals: 1 teams: - locks-review - - min_approvals: 1 + - minApprovals: 1 teams: - polkadot-review @@ -83,7 +83,7 @@ rules: condition: include: - ^bridges/.* - min_approvals: 1 + minApprovals: 1 teams: - bridges-core @@ -95,10 +95,10 @@ rules: - ^substrate/frame/(?!.*(nfts/.*|uniques/.*|babe/.*|grandpa/.*|beefy|merkle-mountain-range/.*|contracts/.*|election|nomination-pools/.*|staking/.*|aura/.*)) type: "and" reviewers: - - min_approvals: 2 + - minApprovals: 2 teams: - core-devs - - min_approvals: 1 + - minApprovals: 1 teams: - frame-coders @@ -107,15 +107,14 @@ rules: condition: include: - review-bot\.yml - min_approvals: 2 type: "and" reviewers: - - min_approvals: 1 + - minApprovals: 1 teams: - opstooling - - min_approvals: 1 + - minApprovals: 1 teams: - locks-review - - min_approvals: 1 + - minApprovals: 1 teams: - ci diff --git a/.github/workflows/review-bot.yml b/.github/workflows/review-bot.yml index aeb33b5da3d3b..b9799935abe67 100644 --- a/.github/workflows/review-bot.yml +++ b/.github/workflows/review-bot.yml @@ -24,7 +24,7 @@ jobs: app_id: ${{ secrets.REVIEW_APP_ID }} private_key: ${{ secrets.REVIEW_APP_KEY }} - name: "Evaluates PR reviews and assigns reviewers" - uses: paritytech/review-bot@v1.1.0 + uses: paritytech/review-bot@v2.0.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} team-token: ${{ steps.team_token.outputs.token }} From f4827dcd16fc5585a8d15c6a32bc27ecde00e7f9 Mon Sep 17 00:00:00 2001 From: yjh <yjh465402634@gmail.com> Date: Wed, 4 Oct 2023 16:36:06 +0800 Subject: [PATCH 05/54] remove outdated scripts (#1769) --- polkadot/scripts/build-demos.sh | 28 ---------------------------- polkadot/scripts/run_all_benches.sh | 15 --------------- 2 files changed, 43 deletions(-) delete mode 100755 polkadot/scripts/build-demos.sh delete mode 100755 polkadot/scripts/run_all_benches.sh diff --git a/polkadot/scripts/build-demos.sh b/polkadot/scripts/build-demos.sh deleted file mode 100755 index 285da143c17d8..0000000000000 --- a/polkadot/scripts/build-demos.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash - -# This script assumes that all pre-requisites are installed. - -set -e - -PROJECT_ROOT=`git rev-parse --show-toplevel` -source `dirname "$0"`/common.sh - -export CARGO_INCREMENTAL=0 - -# Save current directory. -pushd . - -cd $ROOT - -for DEMO in "${DEMOS[@]}" -do - echo "*** Building wasm binaries in $DEMO" - cd "$PROJECT_ROOT/$DEMO" - - ./build.sh - - cd - >> /dev/null -done - -# Restore initial directory. -popd diff --git a/polkadot/scripts/run_all_benches.sh b/polkadot/scripts/run_all_benches.sh deleted file mode 100755 index 923013f351555..0000000000000 --- a/polkadot/scripts/run_all_benches.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# Runs all benchmarks for all pallets, for each of the runtimes specified below -# Should be run on a reference machine to gain accurate benchmarks -# current reference machine: https://github.com/paritytech/substrate/pull/5848 - -runtimes=( - polkadot - kusama - westend -) - -for runtime in "${runtimes[@]}"; do - "$(dirname "$0")/run_benches_for_runtime.sh" "$runtime" -done From 0a6dfdf973b8e7b669eda6e2ed202fb3549a20b9 Mon Sep 17 00:00:00 2001 From: Bradley Olson <34992650+BradleyOlson64@users.noreply.github.com> Date: Wed, 4 Oct 2023 12:27:59 -0700 Subject: [PATCH 06/54] Updating glutton for async backing (#1619) Applied changes from the [User Update Guide](https://docs.google.com/document/d/1WQijD3bZTCsudOyPcDvugv659nCa2hEp2b_8eRU0h-Q), diverging in the node side where service.rs is different for `polkadot-parachain` than in the parachain template. --- Cargo.lock | 2 + .../glutton/glutton-kusama/Cargo.toml | 2 + .../glutton/glutton-kusama/src/lib.rs | 58 ++-- cumulus/polkadot-parachain/Cargo.toml | 1 + cumulus/polkadot-parachain/src/command.rs | 28 +- cumulus/polkadot-parachain/src/service.rs | 287 +++++++++++++++++- 6 files changed, 345 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3ffd4b9fb681..46f6b6ea1fc9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5739,6 +5739,7 @@ dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", "cumulus-pallet-xcm", + "cumulus-primitives-aura", "cumulus-primitives-core", "cumulus-primitives-timestamp", "frame-benchmarking", @@ -12325,6 +12326,7 @@ dependencies = [ "cumulus-client-consensus-proposer", "cumulus-client-consensus-relay-chain", "cumulus-client-service", + "cumulus-primitives-aura", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", "cumulus-relay-chain-interface", diff --git a/cumulus/parachains/runtimes/glutton/glutton-kusama/Cargo.toml b/cumulus/parachains/runtimes/glutton/glutton-kusama/Cargo.toml index 63b658ca977a5..ad13bf05a3ecc 100644 --- a/cumulus/parachains/runtimes/glutton/glutton-kusama/Cargo.toml +++ b/cumulus/parachains/runtimes/glutton/glutton-kusama/Cargo.toml @@ -43,6 +43,7 @@ xcm-executor = { package = "staging-xcm-executor", path = "../../../../../polkad cumulus-pallet-aura-ext = { path = "../../../../pallets/aura-ext", default-features = false } cumulus-pallet-parachain-system = { path = "../../../../pallets/parachain-system", default-features = false, features = ["parameterized-consensus-hook",] } cumulus-pallet-xcm = { path = "../../../../pallets/xcm", default-features = false } +cumulus-primitives-aura = { path = "../../../../primitives/aura", default-features = false } cumulus-primitives-core = { path = "../../../../primitives/core", default-features = false } cumulus-primitives-timestamp = { path = "../../../../primitives/timestamp", default-features = false } parachain-info = { path = "../../../pallets/parachain-info", default-features = false } @@ -72,6 +73,7 @@ std = [ "cumulus-pallet-aura-ext/std", "cumulus-pallet-parachain-system/std", "cumulus-pallet-xcm/std", + "cumulus-primitives-aura/std", "cumulus-primitives-core/std", "cumulus-primitives-timestamp/std", "frame-benchmarking?/std", diff --git a/cumulus/parachains/runtimes/glutton/glutton-kusama/src/lib.rs b/cumulus/parachains/runtimes/glutton/glutton-kusama/src/lib.rs index d3369202aac47..f5d52239e5437 100644 --- a/cumulus/parachains/runtimes/glutton/glutton-kusama/src/lib.rs +++ b/cumulus/parachains/runtimes/glutton/glutton-kusama/src/lib.rs @@ -81,12 +81,7 @@ use frame_system::{ limits::{BlockLength, BlockWeights}, EnsureRoot, }; -use parachains_common::{ - kusama::consensus::{ - BLOCK_PROCESSING_VELOCITY, RELAY_CHAIN_SLOT_DURATION_MILLIS, UNINCLUDED_SEGMENT_CAPACITY, - }, - AccountId, Signature, SLOT_DURATION, -}; +use parachains_common::{AccountId, Signature}; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; pub use sp_runtime::{Perbill, Permill}; @@ -123,10 +118,28 @@ const AVERAGE_ON_INITIALIZE_RATIO: Perbill = Perbill::from_percent(10); const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75); /// We allow for .5 seconds of compute with a 12 second average block time. const MAXIMUM_BLOCK_WEIGHT: Weight = Weight::from_parts( - WEIGHT_REF_TIME_PER_SECOND.saturating_div(2), + WEIGHT_REF_TIME_PER_SECOND.saturating_mul(2), cumulus_primitives_core::relay_chain::MAX_POV_SIZE as u64, ); +/// Maximum number of blocks simultaneously accepted by the Runtime, not yet included +/// into the relay chain. +const UNINCLUDED_SEGMENT_CAPACITY: u32 = 3; +/// How many parachain blocks are processed by the relay chain per parent. Limits the +/// number of blocks authored per slot. +const BLOCK_PROCESSING_VELOCITY: u32 = 2; +/// Relay chain slot duration, in milliseconds. +const RELAY_CHAIN_SLOT_DURATION_MILLIS: u32 = 6000; + +/// This determines the average expected block time that we are targeting. +/// Blocks will be produced at a minimum duration defined by `SLOT_DURATION`. +/// `SLOT_DURATION` is picked up by `pallet_timestamp` which is in turn picked +/// up by `pallet_aura` to implement `fn slot_duration()`. +/// +/// Change this to adjust the block time. +pub const MILLISECS_PER_BLOCK: u64 = 6000; +pub const SLOT_DURATION: u64 = MILLISECS_PER_BLOCK; + parameter_types! { pub const BlockHashCount: BlockNumber = 4096; pub const Version: RuntimeVersion = VERSION; @@ -184,6 +197,13 @@ parameter_types! { pub const ReservedDmpWeight: Weight = MAXIMUM_BLOCK_WEIGHT.saturating_div(2); } +type ConsensusHook = cumulus_pallet_aura_ext::FixedVelocityConsensusHook< + Runtime, + RELAY_CHAIN_SLOT_DURATION_MILLIS, + BLOCK_PROCESSING_VELOCITY, + UNINCLUDED_SEGMENT_CAPACITY, +>; + impl cumulus_pallet_parachain_system::Config for Runtime { type RuntimeEvent = RuntimeEvent; type OnSystemEvent = (); @@ -194,12 +214,7 @@ impl cumulus_pallet_parachain_system::Config for Runtime { type XcmpMessageHandler = (); type ReservedXcmpWeight = (); type CheckAssociatedRelayNumber = RelayNumberMonotonicallyIncreases; - type ConsensusHook = cumulus_pallet_aura_ext::FixedVelocityConsensusHook< - Runtime, - RELAY_CHAIN_SLOT_DURATION_MILLIS, - BLOCK_PROCESSING_VELOCITY, - UNINCLUDED_SEGMENT_CAPACITY, - >; + type ConsensusHook = ConsensusHook; } impl parachain_info::Config for Runtime {} @@ -209,7 +224,7 @@ impl cumulus_pallet_aura_ext::Config for Runtime {} impl pallet_timestamp::Config for Runtime { type Moment = u64; type OnTimestampSet = Aura; - type MinimumPeriod = ConstU64<{ SLOT_DURATION / 2 }>; + type MinimumPeriod = ConstU64<0>; type WeightInfo = weights::pallet_timestamp::WeightInfo<Runtime>; } @@ -217,9 +232,9 @@ impl pallet_aura::Config for Runtime { type AuthorityId = AuraId; type DisabledValidators = (); type MaxAuthorities = ConstU32<100_000>; - type AllowMultipleBlocksPerSlot = ConstBool<false>; + type AllowMultipleBlocksPerSlot = ConstBool<true>; #[cfg(feature = "experimental")] - type SlotDuration = pallet_aura::MinimumPeriodTimesTwo<Self>; + type SlotDuration = ConstU64<SLOT_DURATION>; } impl pallet_glutton::Config for Runtime { @@ -340,7 +355,7 @@ impl_runtime_apis! { impl sp_consensus_aura::AuraApi<Block, AuraId> for Runtime { fn slot_duration() -> sp_consensus_aura::SlotDuration { - sp_consensus_aura::SlotDuration::from_millis(Aura::slot_duration()) + sp_consensus_aura::SlotDuration::from_millis(SLOT_DURATION) } fn authorities() -> Vec<AuraId> { @@ -348,6 +363,15 @@ impl_runtime_apis! { } } + impl cumulus_primitives_aura::AuraUnincludedSegmentApi<Block> for Runtime { + fn can_build_upon( + included_hash: <Block as BlockT>::Hash, + slot: cumulus_primitives_aura::Slot, + ) -> bool { + ConsensusHook::can_build_upon(included_hash, slot) + } + } + impl sp_block_builder::BlockBuilder<Block> for Runtime { fn apply_extrinsic( extrinsic: <Block as BlockT>::Extrinsic, diff --git a/cumulus/polkadot-parachain/Cargo.toml b/cumulus/polkadot-parachain/Cargo.toml index ac8ad53b52431..9d5a5ecda1e68 100644 --- a/cumulus/polkadot-parachain/Cargo.toml +++ b/cumulus/polkadot-parachain/Cargo.toml @@ -87,6 +87,7 @@ cumulus-client-consensus-relay-chain = { path = "../client/consensus/relay-chain cumulus-client-consensus-common = { path = "../client/consensus/common" } cumulus-client-consensus-proposer = { path = "../client/consensus/proposer" } cumulus-client-service = { path = "../client/service" } +cumulus-primitives-aura = { path = "../primitives/aura" } cumulus-primitives-core = { path = "../primitives/core" } cumulus-primitives-parachain-inherent = { path = "../primitives/parachain-inherent" } cumulus-relay-chain-interface = { path = "../client/relay-chain-interface" } diff --git a/cumulus/polkadot-parachain/src/command.rs b/cumulus/polkadot-parachain/src/command.rs index b96163f63e43a..0e948d24f82da 100644 --- a/cumulus/polkadot-parachain/src/command.rs +++ b/cumulus/polkadot-parachain/src/command.rs @@ -876,12 +876,17 @@ pub fn run() -> Result<()> { .await .map(|r| r.0) .map_err(Into::into), - Runtime::Seedling => crate::service::start_shell_node::< - seedling_runtime::RuntimeApi, - >(config, polkadot_config, collator_options, id, hwbench) - .await - .map(|r| r.0) - .map_err(Into::into), + Runtime::Seedling => + crate::service::start_shell_node::<seedling_runtime::RuntimeApi>( + config, + polkadot_config, + collator_options, + id, + hwbench + ) + .await + .map(|r| r.0) + .map_err(Into::into), Runtime::ContractsRococo => crate::service::start_contracts_rococo_node( config, polkadot_config, @@ -949,13 +954,10 @@ pub fn run() -> Result<()> { .map(|r| r.0) .map_err(Into::into), Runtime::Glutton => - crate::service::start_shell_node::<glutton_runtime::RuntimeApi>( - config, - polkadot_config, - collator_options, - id, - hwbench, - ) + crate::service::start_basic_lookahead_node::< + glutton_runtime::RuntimeApi, + AuraId, + >(config, polkadot_config, collator_options, id, hwbench) .await .map(|r| r.0) .map_err(Into::into), diff --git a/cumulus/polkadot-parachain/src/service.rs b/cumulus/polkadot-parachain/src/service.rs index f7b053b4b6a9d..2f86f54f12a1c 100644 --- a/cumulus/polkadot-parachain/src/service.rs +++ b/cumulus/polkadot-parachain/src/service.rs @@ -17,8 +17,9 @@ use codec::Codec; use cumulus_client_cli::CollatorOptions; use cumulus_client_collator::service::CollatorService; -use cumulus_client_consensus_aura::collators::basic::{ - self as basic_aura, Params as BasicAuraParams, +use cumulus_client_consensus_aura::collators::{ + basic::{self as basic_aura, Params as BasicAuraParams}, + lookahead::{self as aura, Params as AuraParams}, }; use cumulus_client_consensus_common::{ ParachainBlockImport as TParachainBlockImport, ParachainCandidate, ParachainConsensus, @@ -31,7 +32,7 @@ use cumulus_client_service::{ BuildNetworkParams, CollatorSybilResistance, DARecoveryProfile, StartRelayChainTasksParams, }; use cumulus_primitives_core::{ - relay_chain::{Hash as PHash, PersistedValidationData}, + relay_chain::{Hash as PHash, PersistedValidationData, ValidationCode}, ParaId, }; use cumulus_relay_chain_interface::{OverseerHandle, RelayChainInterface}; @@ -696,6 +697,188 @@ where Ok((task_manager, client)) } +/// Start a node with the given parachain `Configuration` and relay chain `Configuration`. +/// +/// This is the actual implementation that is abstract over the executor and the runtime api. +/// +/// This node is basic in the sense that it doesn't support functionality like transaction +/// payment. Intended to replace start_shell_node in use for glutton, shell, and seedling. +#[sc_tracing::logging::prefix_logs_with("Parachain")] +async fn start_basic_lookahead_node_impl<RuntimeApi, RB, BIQ, SC>( + parachain_config: Configuration, + polkadot_config: Configuration, + collator_options: CollatorOptions, + sybil_resistance_level: CollatorSybilResistance, + para_id: ParaId, + rpc_ext_builder: RB, + build_import_queue: BIQ, + start_consensus: SC, + hwbench: Option<sc_sysinfo::HwBench>, +) -> sc_service::error::Result<(TaskManager, Arc<ParachainClient<RuntimeApi>>)> +where + RuntimeApi: ConstructRuntimeApi<Block, ParachainClient<RuntimeApi>> + Send + Sync + 'static, + RuntimeApi::RuntimeApi: sp_transaction_pool::runtime_api::TaggedTransactionQueue<Block> + + sp_api::Metadata<Block> + + sp_session::SessionKeys<Block> + + sp_api::ApiExt<Block> + + sp_offchain::OffchainWorkerApi<Block> + + sp_block_builder::BlockBuilder<Block> + + cumulus_primitives_core::CollectCollationInfo<Block> + + frame_rpc_system::AccountNonceApi<Block, AccountId, Nonce>, + RB: Fn(Arc<ParachainClient<RuntimeApi>>) -> Result<jsonrpsee::RpcModule<()>, sc_service::Error> + + 'static, + BIQ: FnOnce( + Arc<ParachainClient<RuntimeApi>>, + ParachainBlockImport<RuntimeApi>, + &Configuration, + Option<TelemetryHandle>, + &TaskManager, + ) -> Result<sc_consensus::DefaultImportQueue<Block>, sc_service::Error>, + SC: FnOnce( + Arc<ParachainClient<RuntimeApi>>, + ParachainBlockImport<RuntimeApi>, + Option<&Registry>, + Option<TelemetryHandle>, + &TaskManager, + Arc<dyn RelayChainInterface>, + Arc<sc_transaction_pool::FullPool<Block, ParachainClient<RuntimeApi>>>, + Arc<SyncingService<Block>>, + KeystorePtr, + Duration, + ParaId, + CollatorPair, + OverseerHandle, + Arc<dyn Fn(Hash, Option<Vec<u8>>) + Send + Sync>, + Arc<ParachainBackend>, + ) -> Result<(), sc_service::Error>, +{ + let parachain_config = prepare_node_config(parachain_config); + + let params = new_partial::<RuntimeApi, BIQ>(¶chain_config, build_import_queue)?; + let (block_import, mut telemetry, telemetry_worker_handle) = params.other; + + let client = params.client.clone(); + let backend = params.backend.clone(); + + let mut task_manager = params.task_manager; + let (relay_chain_interface, collator_key) = build_relay_chain_interface( + polkadot_config, + ¶chain_config, + telemetry_worker_handle, + &mut task_manager, + collator_options.clone(), + hwbench.clone(), + ) + .await + .map_err(|e| sc_service::Error::Application(Box::new(e) as Box<_>))?; + + let validator = parachain_config.role.is_authority(); + let prometheus_registry = parachain_config.prometheus_registry().cloned(); + let transaction_pool = params.transaction_pool.clone(); + let import_queue_service = params.import_queue.service(); + let net_config = FullNetworkConfiguration::new(¶chain_config.network); + + let (network, system_rpc_tx, tx_handler_controller, start_network, sync_service) = + build_network(BuildNetworkParams { + parachain_config: ¶chain_config, + net_config, + client: client.clone(), + transaction_pool: transaction_pool.clone(), + para_id, + spawn_handle: task_manager.spawn_handle(), + relay_chain_interface: relay_chain_interface.clone(), + import_queue: params.import_queue, + sybil_resistance_level, + }) + .await?; + + let rpc_client = client.clone(); + let rpc_builder = Box::new(move |_, _| rpc_ext_builder(rpc_client.clone())); + + sc_service::spawn_tasks(sc_service::SpawnTasksParams { + rpc_builder, + client: client.clone(), + transaction_pool: transaction_pool.clone(), + task_manager: &mut task_manager, + config: parachain_config, + keystore: params.keystore_container.keystore(), + backend: backend.clone(), + network: network.clone(), + sync_service: sync_service.clone(), + system_rpc_tx, + tx_handler_controller, + telemetry: telemetry.as_mut(), + })?; + + if let Some(hwbench) = hwbench { + sc_sysinfo::print_hwbench(&hwbench); + if validator { + warn_if_slow_hardware(&hwbench); + } + + if let Some(ref mut telemetry) = telemetry { + let telemetry_handle = telemetry.handle(); + task_manager.spawn_handle().spawn( + "telemetry_hwbench", + None, + sc_sysinfo::initialize_hwbench_telemetry(telemetry_handle, hwbench), + ); + } + } + + let announce_block = { + let sync_service = sync_service.clone(); + Arc::new(move |hash, data| sync_service.announce_block(hash, data)) + }; + + let relay_chain_slot_duration = Duration::from_secs(6); + + let overseer_handle = relay_chain_interface + .overseer_handle() + .map_err(|e| sc_service::Error::Application(Box::new(e)))?; + + start_relay_chain_tasks(StartRelayChainTasksParams { + client: client.clone(), + announce_block: announce_block.clone(), + para_id, + relay_chain_interface: relay_chain_interface.clone(), + task_manager: &mut task_manager, + da_recovery_profile: if validator { + DARecoveryProfile::Collator + } else { + DARecoveryProfile::FullNode + }, + import_queue: import_queue_service, + relay_chain_slot_duration, + recovery_handle: Box::new(overseer_handle.clone()), + sync_service: sync_service.clone(), + })?; + + if validator { + start_consensus( + client.clone(), + block_import, + prometheus_registry.as_ref(), + telemetry.as_ref().map(|t| t.handle()), + &task_manager, + relay_chain_interface.clone(), + transaction_pool, + sync_service.clone(), + params.keystore_container.keystore(), + relay_chain_slot_duration, + para_id, + collator_key.expect("Command line arguments do not allow this. qed"), + overseer_handle, + announce_block, + backend.clone(), + )?; + } + + start_network.start_network(); + + Ok((task_manager, client)) +} + /// Build the import queue for the rococo parachain runtime. pub fn rococo_parachain_build_import_queue( client: Arc<ParachainClient<rococo_parachain_runtime::RuntimeApi>>, @@ -1206,6 +1389,104 @@ where .await } +/// Start an aura powered parachain node which uses the lookahead collator to support async backing. +/// This node is basic in the sense that its runtime api doesn't include common contents such as +/// transaction payment. Used for aura glutton. +pub async fn start_basic_lookahead_node<RuntimeApi, AuraId: AppCrypto>( + parachain_config: Configuration, + polkadot_config: Configuration, + collator_options: CollatorOptions, + para_id: ParaId, + hwbench: Option<sc_sysinfo::HwBench>, +) -> sc_service::error::Result<(TaskManager, Arc<ParachainClient<RuntimeApi>>)> +where + RuntimeApi: ConstructRuntimeApi<Block, ParachainClient<RuntimeApi>> + Send + Sync + 'static, + RuntimeApi::RuntimeApi: sp_transaction_pool::runtime_api::TaggedTransactionQueue<Block> + + sp_api::Metadata<Block> + + sp_session::SessionKeys<Block> + + sp_api::ApiExt<Block> + + sp_offchain::OffchainWorkerApi<Block> + + sp_block_builder::BlockBuilder<Block> + + cumulus_primitives_core::CollectCollationInfo<Block> + + sp_consensus_aura::AuraApi<Block, <<AuraId as AppCrypto>::Pair as Pair>::Public> + + frame_rpc_system::AccountNonceApi<Block, AccountId, Nonce> + + cumulus_primitives_aura::AuraUnincludedSegmentApi<Block>, + <<AuraId as AppCrypto>::Pair as Pair>::Signature: + TryFrom<Vec<u8>> + std::hash::Hash + sp_runtime::traits::Member + Codec, +{ + start_basic_lookahead_node_impl::<RuntimeApi, _, _, _>( + parachain_config, + polkadot_config, + collator_options, + CollatorSybilResistance::Resistant, // Aura + para_id, + |_| Ok(RpcModule::new(())), + aura_build_import_queue::<_, AuraId>, + |client, + block_import, + prometheus_registry, + telemetry, + task_manager, + relay_chain_interface, + transaction_pool, + sync_oracle, + keystore, + relay_chain_slot_duration, + para_id, + collator_key, + overseer_handle, + announce_block, + backend| { + let slot_duration = cumulus_client_consensus_aura::slot_duration(&*client)?; + + let proposer_factory = sc_basic_authorship::ProposerFactory::with_proof_recording( + task_manager.spawn_handle(), + client.clone(), + transaction_pool, + prometheus_registry, + telemetry.clone(), + ); + let proposer = Proposer::new(proposer_factory); + + let collator_service = CollatorService::new( + client.clone(), + Arc::new(task_manager.spawn_handle()), + announce_block, + client.clone(), + ); + + let params = AuraParams { + create_inherent_data_providers: move |_, ()| async move { Ok(()) }, + block_import, + para_client: client.clone(), + para_backend: backend.clone(), + relay_client: relay_chain_interface, + code_hash_provider: move |block_hash| { + client.code_at(block_hash).ok().map(ValidationCode).map(|c| c.hash()) + }, + sync_oracle, + keystore, + collator_key, + para_id, + overseer_handle, + slot_duration, + relay_chain_slot_duration, + proposer, + collator_service, + authoring_duration: Duration::from_millis(1500), + }; + + let fut = + aura::run::<Block, <AuraId as AppCrypto>::Pair, _, _, _, _, _, _, _, _, _>(params); + task_manager.spawn_essential_handle().spawn("aura", None, fut); + + Ok(()) + }, + hwbench, + ) + .await +} + #[sc_tracing::logging::prefix_logs_with("Parachain")] async fn start_contracts_rococo_node_impl<RuntimeApi, RB, BIQ, SC>( parachain_config: Configuration, From 86955eef90138941beabf4143b645d652ac98d14 Mon Sep 17 00:00:00 2001 From: Sergejs Kostjucenko <85877331+sergejparity@users.noreply.github.com> Date: Thu, 5 Oct 2023 18:05:42 +0300 Subject: [PATCH 07/54] Remove deprecated CI config files (#1799) This PR removes deprecated CI config files --- cumulus/.gitattributes | 2 - cumulus/.github/dependabot.yml | 31 -- cumulus/.github/pr-custom-review.yml | 48 -- cumulus/.github/workflows/check-D-labels.yml | 47 -- cumulus/.github/workflows/check-labels.yml | 45 -- cumulus/.github/workflows/docs.yml | 39 -- cumulus/.github/workflows/fmt-check.yml | 22 - .../.github/workflows/pr-custom-review.yml | 42 -- .../workflows/release-01_branch-check.yml | 22 - .../workflows/release-10_rc-automation.yml | 87 ---- ...e-20_extrinsic-ordering-check-from-bin.yml | 86 ---- ...e-21_extrinsic-ordering-check-from-two.yml | 120 ----- .../workflows/release-30_create-draft.yml | 311 ------------- .../workflows/release-99_bot-announce.yml | 39 -- cumulus/.github/workflows/srtool.yml | 122 ------ cumulus/.gitlab-ci.yml | 201 --------- cumulus/scripts/ci/changelog/README.md | 77 ---- polkadot/.gitattributes | 2 - polkadot/.github/dependabot.yml | 26 -- polkadot/.github/pr-custom-review.yml | 42 -- .../workflows/burnin-label-notification.yml | 24 - polkadot/.github/workflows/check-D-labels.yml | 50 --- .../.github/workflows/check-bootnodes.yml | 31 -- polkadot/.github/workflows/check-labels.yml | 45 -- polkadot/.github/workflows/check-licenses.yml | 26 -- .../.github/workflows/check-new-bootnodes.yml | 28 -- polkadot/.github/workflows/check-weights.yml | 49 --- polkadot/.github/workflows/honggfuzz.yml | 137 ------ .../.github/workflows/pr-custom-review.yml | 42 -- .../workflows/release-01_branch-check.yml | 21 - .../workflows/release-10_candidate.yml | 71 --- ...e-20_extrinsic-ordering-check-from-bin.yml | 81 ---- ...e-21_extrinsic-ordering-check-from-two.yml | 97 ----- .../release-30_publish-draft-release.yml | 199 --------- polkadot/.github/workflows/release-99_bot.yml | 49 --- polkadot/.gitlab-ci.yml | 287 ------------ substrate/.gitattributes | 2 - substrate/.github/dependabot.yml | 12 - substrate/.github/pr-custom-review.yml | 39 -- substrate/.github/stale.yml | 18 - .../.github/workflows/auto-label-issues.yml | 17 - .../workflows/burnin-label-notification.yml | 24 - .../.github/workflows/check-D-labels.yml | 48 -- substrate/.github/workflows/check-labels.yml | 45 -- substrate/.github/workflows/md-link-check.yml | 19 - substrate/.github/workflows/mlc_config.json | 7 - substrate/.github/workflows/monthly-tag.yml | 43 -- .../.github/workflows/pr-custom-review.yml | 42 -- substrate/.github/workflows/release-bot.yml | 31 -- .../.github/workflows/release-tagging.yml | 20 - substrate/.gitlab-ci.yml | 412 ------------------ 51 files changed, 3427 deletions(-) delete mode 100644 cumulus/.gitattributes delete mode 100644 cumulus/.github/dependabot.yml delete mode 100644 cumulus/.github/pr-custom-review.yml delete mode 100644 cumulus/.github/workflows/check-D-labels.yml delete mode 100644 cumulus/.github/workflows/check-labels.yml delete mode 100644 cumulus/.github/workflows/docs.yml delete mode 100644 cumulus/.github/workflows/fmt-check.yml delete mode 100644 cumulus/.github/workflows/pr-custom-review.yml delete mode 100644 cumulus/.github/workflows/release-01_branch-check.yml delete mode 100644 cumulus/.github/workflows/release-10_rc-automation.yml delete mode 100644 cumulus/.github/workflows/release-20_extrinsic-ordering-check-from-bin.yml delete mode 100644 cumulus/.github/workflows/release-21_extrinsic-ordering-check-from-two.yml delete mode 100644 cumulus/.github/workflows/release-30_create-draft.yml delete mode 100644 cumulus/.github/workflows/release-99_bot-announce.yml delete mode 100644 cumulus/.github/workflows/srtool.yml delete mode 100644 cumulus/.gitlab-ci.yml delete mode 100644 cumulus/scripts/ci/changelog/README.md delete mode 100644 polkadot/.gitattributes delete mode 100644 polkadot/.github/dependabot.yml delete mode 100644 polkadot/.github/pr-custom-review.yml delete mode 100644 polkadot/.github/workflows/burnin-label-notification.yml delete mode 100644 polkadot/.github/workflows/check-D-labels.yml delete mode 100644 polkadot/.github/workflows/check-bootnodes.yml delete mode 100644 polkadot/.github/workflows/check-labels.yml delete mode 100644 polkadot/.github/workflows/check-licenses.yml delete mode 100644 polkadot/.github/workflows/check-new-bootnodes.yml delete mode 100644 polkadot/.github/workflows/check-weights.yml delete mode 100644 polkadot/.github/workflows/honggfuzz.yml delete mode 100644 polkadot/.github/workflows/pr-custom-review.yml delete mode 100644 polkadot/.github/workflows/release-01_branch-check.yml delete mode 100644 polkadot/.github/workflows/release-10_candidate.yml delete mode 100644 polkadot/.github/workflows/release-20_extrinsic-ordering-check-from-bin.yml delete mode 100644 polkadot/.github/workflows/release-21_extrinsic-ordering-check-from-two.yml delete mode 100644 polkadot/.github/workflows/release-30_publish-draft-release.yml delete mode 100644 polkadot/.github/workflows/release-99_bot.yml delete mode 100644 polkadot/.gitlab-ci.yml delete mode 100644 substrate/.github/dependabot.yml delete mode 100644 substrate/.github/pr-custom-review.yml delete mode 100644 substrate/.github/stale.yml delete mode 100644 substrate/.github/workflows/auto-label-issues.yml delete mode 100644 substrate/.github/workflows/burnin-label-notification.yml delete mode 100644 substrate/.github/workflows/check-D-labels.yml delete mode 100644 substrate/.github/workflows/check-labels.yml delete mode 100644 substrate/.github/workflows/md-link-check.yml delete mode 100644 substrate/.github/workflows/mlc_config.json delete mode 100644 substrate/.github/workflows/monthly-tag.yml delete mode 100644 substrate/.github/workflows/pr-custom-review.yml delete mode 100644 substrate/.github/workflows/release-bot.yml delete mode 100644 substrate/.github/workflows/release-tagging.yml delete mode 100644 substrate/.gitlab-ci.yml diff --git a/cumulus/.gitattributes b/cumulus/.gitattributes deleted file mode 100644 index 2ea1ab2d6b9cf..0000000000000 --- a/cumulus/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -/.gitlab-ci.yml filter=ci-prettier -/scripts/ci/gitlab/pipeline/*.yml filter=ci-prettier diff --git a/cumulus/.github/dependabot.yml b/cumulus/.github/dependabot.yml deleted file mode 100644 index 349a34690d4eb..0000000000000 --- a/cumulus/.github/dependabot.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "cargo" - directory: "/" - labels: ["A2-insubstantial", "B0-silent", "C1-low"] - # Handle updates for crates from github.com/paritytech/substrate manually. - ignore: - - dependency-name: "substrate-*" - - dependency-name: "sc-*" - - dependency-name: "sp-*" - - dependency-name: "frame-*" - - dependency-name: "fork-tree" - - dependency-name: "frame-remote-externalities" - - dependency-name: "pallet-*" - - dependency-name: "beefy-*" - - dependency-name: "try-runtime-*" - - dependency-name: "test-runner" - - dependency-name: "generate-bags" - - dependency-name: "sub-tokens" - - dependency-name: "polkadot-*" - - dependency-name: "xcm*" - - dependency-name: "kusama-*" - - dependency-name: "westend-*" - - dependency-name: "rococo-*" - schedule: - interval: "daily" - - package-ecosystem: github-actions - directory: '/' - labels: ["A2-insubstantial", "B0-silent", "C1-low", "E2-dependencies"] - schedule: - interval: daily diff --git a/cumulus/.github/pr-custom-review.yml b/cumulus/.github/pr-custom-review.yml deleted file mode 100644 index fc26ee677f065..0000000000000 --- a/cumulus/.github/pr-custom-review.yml +++ /dev/null @@ -1,48 +0,0 @@ -# 🔒 PROTECTED: Changes to locks-review-team should be approved by the current locks-review-team -locks-review-team: cumulus-locks-review -team-leads-team: polkadot-review -action-review-team: ci - -rules: - - name: Runtime files - check_type: changed_files - condition: ^parachains/runtimes/assets/(asset-hub-kusama|asset-hub-polkadot)/src/[^/]+\.rs$|^parachains/runtimes/bridge-hubs/(bridge-hub-kusama|bridge-hub-polkadot)/src/[^/]+\.rs$|^parachains/runtimes/collectives/collectives-polkadot/src/[^/]+\.rs$|^parachains/common/src/[^/]+\.rs$ - all_distinct: - - min_approvals: 1 - teams: - - cumulus-locks-review - - min_approvals: 1 - teams: - - polkadot-review - - - name: Core developers - check_type: changed_files - condition: - include: .* - # excluding files from 'Runtime files' and 'CI files' rules and `Bridges subtree files` - exclude: ^parachains/runtimes/assets/(asset-hub-kusama|asset-hub-polkadot)/src/[^/]+\.rs$|^parachains/runtimes/bridge-hubs/(bridge-hub-kusama|bridge-hub-polkadot)/src/[^/]+\.rs$|^parachains/runtimes/collectives/collectives-polkadot/src/[^/]+\.rs$|^parachains/common/src/[^/]+\.rs$|^\.gitlab-ci\.yml|^scripts/ci/.*|^\.github/.* - min_approvals: 2 - teams: - - core-devs - - # if there are any changes in the bridges subtree (in case of backport changes back to bridges repo) - - name: Bridges subtree files - check_type: changed_files - condition: ^bridges/.* - min_approvals: 1 - teams: - - bridges-core - - - name: CI files - check_type: changed_files - condition: - include: ^\.gitlab-ci\.yml|^scripts/ci/.*|^\.github/.* - exclude: ^scripts/ci/gitlab/pipeline/zombienet.yml$ - min_approvals: 2 - teams: - - ci - - release-engineering - -prevent-review-request: - teams: - - core-devs diff --git a/cumulus/.github/workflows/check-D-labels.yml b/cumulus/.github/workflows/check-D-labels.yml deleted file mode 100644 index 9106272093107..0000000000000 --- a/cumulus/.github/workflows/check-D-labels.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Check D labels - -on: - pull_request: - types: [labeled, opened, synchronize, unlabeled] - paths: - - primitives/** - -jobs: - check-labels: - runs-on: ubuntu-latest - steps: - - name: Pull image - env: - IMAGE: paritytech/ruled_labels:0.4.0 - run: docker pull $IMAGE - - - name: Check labels - env: - IMAGE: paritytech/ruled_labels:0.4.0 - MOUNT: /work - GITHUB_PR: ${{ github.event.pull_request.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - API_BASE: https://api.github.com/repos - REPO: ${{ github.repository }} - RULES_PATH: labels/ruled_labels - CHECK_SPECS: specs_cumulus.yaml - run: | - echo "REPO: ${REPO}" - echo "GITHUB_PR: ${GITHUB_PR}" - # Clone repo with labels specs - git clone https://github.com/paritytech/labels - # Fetch the labels for the PR under test - labels=$( curl -H "Authorization: token ${GITHUB_TOKEN}" -s "$API_BASE/${REPO}/pulls/${GITHUB_PR}" | jq '.labels | .[] | .name' | tr "\n" ",") - - if [ -z "${labels}" ]; then - docker run --rm -i -v $PWD/${RULES_PATH}/:$MOUNT $IMAGE check $MOUNT/$CHECK_SPECS --tags audit --no-label - fi - - labels_args=${labels: :-1} - printf "Checking labels: %s\n" "${labels_args}" - - # Prevent the shell from splitting labels with spaces - IFS="," - - # --dev is more useful to debug mode to debug - docker run --rm -i -v $PWD/${RULES_PATH}/:$MOUNT $IMAGE check $MOUNT/$CHECK_SPECS --labels ${labels_args} --dev --tags audit diff --git a/cumulus/.github/workflows/check-labels.yml b/cumulus/.github/workflows/check-labels.yml deleted file mode 100644 index 004271d7788ad..0000000000000 --- a/cumulus/.github/workflows/check-labels.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Check labels - -on: - pull_request: - types: [labeled, opened, synchronize, unlabeled] - -jobs: - check-labels: - runs-on: ubuntu-latest - steps: - - name: Pull image - env: - IMAGE: paritytech/ruled_labels:0.4.0 - run: docker pull $IMAGE - - - name: Check labels - env: - IMAGE: paritytech/ruled_labels:0.4.0 - MOUNT: /work - GITHUB_PR: ${{ github.event.pull_request.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - API_BASE: https://api.github.com/repos - REPO: ${{ github.repository }} - RULES_PATH: labels/ruled_labels - CHECK_SPECS: specs_cumulus.yaml - run: | - echo "REPO: ${REPO}" - echo "GITHUB_PR: ${GITHUB_PR}" - # Clone repo with labels specs - git clone https://github.com/paritytech/labels - # Fetch the labels for the PR under test - labels=$( curl -H "Authorization: token ${GITHUB_TOKEN}" -s "$API_BASE/${REPO}/pulls/${GITHUB_PR}" | jq '.labels | .[] | .name' | tr "\n" ",") - - if [ -z "${labels}" ]; then - docker run --rm -i -v $PWD/${RULES_PATH}/:$MOUNT $IMAGE check $MOUNT/$CHECK_SPECS --tags audit --no-label - fi - - labels_args=${labels: :-1} - printf "Checking labels: %s\n" "${labels_args}" - - # Prevent the shell from splitting labels with spaces - IFS="," - - # --dev is more useful to debug mode to debug - docker run --rm -i -v $PWD/${RULES_PATH}/:$MOUNT $IMAGE check $MOUNT/$CHECK_SPECS --labels ${labels_args} --dev --tags PR diff --git a/cumulus/.github/workflows/docs.yml b/cumulus/.github/workflows/docs.yml deleted file mode 100644 index 6aab3f27be6b4..0000000000000 --- a/cumulus/.github/workflows/docs.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Publish Rust Docs - -on: - push: - branches: - - master - -jobs: - deploy-docs: - name: Deploy docs - runs-on: ubuntu-latest - - steps: - - name: Install tooling - run: | - sudo apt-get install -y protobuf-compiler - protoc --version - - - name: Checkout repository - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - - name: Rust versions - run: rustup show - - - name: Rust cache - uses: Swatinem/rust-cache@e207df5d269b42b69c8bc5101da26f7d31feddb4 # v2.6.2 - - - name: Build rustdocs - run: SKIP_WASM_BUILD=1 cargo doc --all --no-deps - - - name: Make index.html - run: echo "<meta http-equiv=refresh content=0;url=cumulus_client_collator/index.html>" > ./target/doc/index.html - - - name: Deploy documentation - uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3.9.3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_branch: gh-pages - publish_dir: ./target/doc diff --git a/cumulus/.github/workflows/fmt-check.yml b/cumulus/.github/workflows/fmt-check.yml deleted file mode 100644 index 7571c51116be8..0000000000000 --- a/cumulus/.github/workflows/fmt-check.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Rustfmt check - -on: - push: - branches: - - master - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - -jobs: - quick_check: - strategy: - matrix: - os: ["ubuntu-latest"] - runs-on: ${{ matrix.os }} - container: - image: paritytech/ci-linux:production - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - - name: Cargo fmt - run: cargo +nightly fmt --all -- --check diff --git a/cumulus/.github/workflows/pr-custom-review.yml b/cumulus/.github/workflows/pr-custom-review.yml deleted file mode 100644 index 8e40c9ee72989..0000000000000 --- a/cumulus/.github/workflows/pr-custom-review.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Assign reviewers - -on: - pull_request: - branches: - - master - - main - types: - - opened - - reopened - - synchronize - - review_requested - - review_request_removed - - ready_for_review - - converted_to_draft - pull_request_review: - -jobs: - pr-custom-review: - runs-on: ubuntu-latest - steps: - - name: Skip if pull request is in Draft - # `if: github.event.pull_request.draft == true` should be kept here, at - # the step level, rather than at the job level. The latter is not - # recommended because when the PR is moved from "Draft" to "Ready to - # review" the workflow will immediately be passing (since it was skipped), - # even though it hasn't actually ran, since it takes a few seconds for - # the workflow to start. This is also disclosed in: - # https://github.community/t/dont-run-actions-on-draft-pull-requests/16817/17 - # That scenario would open an opportunity for the check to be bypassed: - # 1. Get your PR approved - # 2. Move it to Draft - # 3. Push whatever commits you want - # 4. Move it to "Ready for review"; now the workflow is passing (it was - # skipped) and "Check reviews" is also passing (it won't be updated - # until the workflow is finished) - if: github.event.pull_request.draft == true - run: exit 1 - - name: pr-custom-review - uses: paritytech/pr-custom-review@action-v3 - with: - checks-reviews-api: http://pcr.parity-prod.parity.io/api/v1/check_reviews diff --git a/cumulus/.github/workflows/release-01_branch-check.yml b/cumulus/.github/workflows/release-01_branch-check.yml deleted file mode 100644 index afcd4580f176f..0000000000000 --- a/cumulus/.github/workflows/release-01_branch-check.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Release branch check -on: - push: - branches: - - release-**v[0-9]+.[0-9]+.[0-9]+ # client - - release-**v[0-9]+ # runtimes - - polkadot-v[0-9]+.[0-9]+.[0-9]+ # cumulus code - - workflow_dispatch: - -jobs: - check_branch: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - with: - fetch-depth: 0 - - - name: Run check - shell: bash - run: ./scripts/ci/github/check-rel-br diff --git a/cumulus/.github/workflows/release-10_rc-automation.yml b/cumulus/.github/workflows/release-10_rc-automation.yml deleted file mode 100644 index d1795faef096e..0000000000000 --- a/cumulus/.github/workflows/release-10_rc-automation.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Release - RC automation -on: - push: - branches: - - release-v[0-9]+.[0-9]+.[0-9]+ - - release-parachains-v[0-9]+ - workflow_dispatch: - -jobs: - tag_rc: - runs-on: ubuntu-latest - strategy: - matrix: - channel: - - name: 'RelEng: Cumulus Release Coordination' - room: '!NAEMyPAHWOiOQHsvus:parity.io' - pre-releases: true - steps: - - name: Checkout sources - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - with: - fetch-depth: 0 - - id: compute_tag - name: Compute next rc tag - shell: bash - run: | - # Get last rc tag if exists, else set it to {version}-rc1 - version=${GITHUB_REF#refs/heads/release-} - echo "$version" - echo "version=$version" >> $GITHUB_OUTPUT - git tag -l - last_rc=$(git tag -l "$version-rc*" | sort -V | tail -n 1) - if [ -n "$last_rc" ]; then - suffix=$(echo "$last_rc" | grep -Eo '[0-9]+$') - echo $suffix - ((suffix++)) - echo $suffix - echo "new_tag=$version-rc$suffix" >> $GITHUB_OUTPUT - echo "first_rc=false" >> $GITHUB_OUTPUT - else - echo "new_tag=$version-rc1" >> $GITHUB_OUTPUT - echo "first_rc=true" >> $GITHUB_OUTPUT - fi - - - name: Apply new tag - uses: tvdias/github-tagger@ed7350546e3e503b5e942dffd65bc8751a95e49d # v0.0.2 - with: - # We can't use the normal GITHUB_TOKEN for the following reason: - # https://docs.github.com/en/actions/reference/events-that-trigger-workflows#triggering-new-workflows-using-a-personal-access-token - # RELEASE_BRANCH_TOKEN requires public_repo OAuth scope - repo-token: "${{ secrets.RELEASE_BRANCH_TOKEN }}" - tag: ${{ steps.compute_tag.outputs.new_tag }} - - - id: create-issue-checklist-client - uses: JasonEtco/create-an-issue@e27dddc79c92bc6e4562f268fffa5ed752639abd # v2.9.1 - # Only create the issue if it's the first release candidate - if: steps.compute_tag.outputs.first_rc == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.compute_tag.outputs.version }} - with: - assignees: EgorPopelyaev, coderobe, chevdor - filename: .github/ISSUE_TEMPLATE/release-client.md - - - id: create-issue-checklist-runtime - uses: JasonEtco/create-an-issue@e27dddc79c92bc6e4562f268fffa5ed752639abd # v2.9.1 - # Only create the issue if it's the first release candidate - if: steps.compute_tag.outputs.first_rc == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.compute_tag.outputs.version }} - with: - assignees: EgorPopelyaev, coderobe, chevdor - filename: .github/ISSUE_TEMPLATE/release-runtime.md - - - name: Matrix notification to ${{ matrix.channel.name }} - uses: s3krit/matrix-message-action@70ad3fb812ee0e45ff8999d6af11cafad11a6ecf # v0.0.3 - if: steps.create-issue-checklist-client.outputs.url != '' && steps.create-issue-checklist-runtime.outputs.url != '' - with: - room_id: ${{ matrix.channel.room }} - access_token: ${{ secrets.RELEASENOTES_MATRIX_V2_ACCESS_TOKEN }} - server: "m.parity.io" - message: | - The Release Process for Cumulus ${{ steps.compute_tag.outputs.version }} has been started.<br/> - Tracking issues: - - client: ${{ steps.create-issue-checklist-client.outputs.url }}" - - runtime: ${{ steps.create-issue-checklist-runtime.outputs.url }}" diff --git a/cumulus/.github/workflows/release-20_extrinsic-ordering-check-from-bin.yml b/cumulus/.github/workflows/release-20_extrinsic-ordering-check-from-bin.yml deleted file mode 100644 index d902e57ac9e7f..0000000000000 --- a/cumulus/.github/workflows/release-20_extrinsic-ordering-check-from-bin.yml +++ /dev/null @@ -1,86 +0,0 @@ -# This workflow performs the Extrinsic Ordering Check on demand using a binary - -name: Release - Extrinsic Ordering Check from Binary -on: - workflow_dispatch: - inputs: - reference_url: - description: The WebSocket url of the reference node - default: wss://kusama-asset-hub-rpc.polkadot.io - required: true - binary_url: - description: A url to a Linux binary for the node containing the runtime to test - default: https://releases.parity.io/cumulus/polkadot-v0.9.21/polkadot-parachain - required: true - chain: - description: The name of the chain under test. Usually, you would pass a local chain - default: asset-hub-kusama-local - required: true - -jobs: - check: - name: Run check - runs-on: ubuntu-latest - env: - CHAIN: ${{github.event.inputs.chain}} - BIN: node-bin - BIN_PATH: ./tmp/$BIN - BIN_URL: ${{github.event.inputs.binary_url}} - REF_URL: ${{github.event.inputs.reference_url}} - - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - - name: Fetch binary - run: | - echo Creating a temp dir to download and run binary - mkdir -p tmp - echo Fetching $BIN_URL - wget $BIN_URL -O $BIN_PATH - chmod a+x $BIN_PATH - $BIN_PATH --version - - - name: Start local node - run: | - echo Running on $CHAIN - $BIN_PATH --chain=$CHAIN -- --chain polkadot-local & - - - name: Prepare output - run: | - VERSION=$($BIN_PATH --version) - echo "Metadata comparison:" >> output.txt - echo "Date: $(date)" >> output.txt - echo "Reference: $REF_URL" >> output.txt - echo "Target version: $VERSION" >> output.txt - echo "Chain: $CHAIN" >> output.txt - echo "----------------------------------------------------------------------" >> output.txt - - - name: Pull polkadot-js-tools image - run: docker pull jacogr/polkadot-js-tools - - - name: Compare the metadata - run: | - CMD="docker run --pull always --network host jacogr/polkadot-js-tools metadata $REF_URL ws://localhost:9944" - echo -e "Running:\n$CMD" - $CMD >> output.txt - sed -z -i 's/\n\n/\n/g' output.txt - cat output.txt | egrep -n -i '' - SUMMARY=$(./scripts/ci/github/extrinsic-ordering-filter.sh output.txt) - echo -e $SUMMARY - echo -e $SUMMARY >> output.txt - - - name: Show result - run: | - cat output.txt - - - name: Stop our local node - run: | - pkill $BIN - continue-on-error: true - - - name: Save output as artifact - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - with: - name: ${{ env.CHAIN }} - path: | - output.txt diff --git a/cumulus/.github/workflows/release-21_extrinsic-ordering-check-from-two.yml b/cumulus/.github/workflows/release-21_extrinsic-ordering-check-from-two.yml deleted file mode 100644 index 93c0050ff6f25..0000000000000 --- a/cumulus/.github/workflows/release-21_extrinsic-ordering-check-from-two.yml +++ /dev/null @@ -1,120 +0,0 @@ -# This workflow performs the Extrinsic Ordering Check on demand using two reference binaries - -name: Release - Extrinsic API Check with reference bins -on: - workflow_dispatch: - inputs: - reference_binary_url: - description: A url to a Linux binary for the node containing the reference runtime to test against - default: https://releases.parity.io/cumulus/v0.9.230/polkadot-parachain - required: true - binary_url: - description: A url to a Linux binary for the node containing the runtime to test - default: https://releases.parity.io/cumulus/v0.9.270-rc7/polkadot-parachain - required: true - -jobs: - check: - name: Run check - runs-on: ubuntu-latest - timeout-minutes: 10 - env: - REF_URL: ${{github.event.inputs.reference_binary_url}} - BIN_REF: polkadot-parachain-ref - BIN_URL: ${{github.event.inputs.binary_url}} - BIN_BASE: polkadot-parachain - TMP: ./tmp - strategy: - fail-fast: false - matrix: - include: - - runtime: asset-hub-kusama - local: asset-hub-kusama-local - relay: kusama-local - - runtime: asset-hub-polkadot - local: asset-hub-polkadot-local - relay: polkadot-local - - runtime: asset-hub-westend - local: asset-hub-westend-local - relay: polkadot-local - - runtime: contracts-rococo - local: contracts-rococo-local - relay: polkadot-local - - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - - name: Create tmp dir - run: | - mkdir -p $TMP - pwd - - - name: Fetch reference binary for ${{ matrix.runtime }} - run: | - echo Fetching $REF_URL - curl $REF_URL -o $TMP/$BIN_REF - chmod a+x $TMP/$BIN_REF - $TMP/$BIN_REF --version - - - name: Fetch test binary for ${{ matrix.runtime }} - run: | - echo Fetching $BIN_URL - curl $BIN_URL -o $TMP/$BIN_BASE - chmod a+x $TMP/$BIN_BASE - $TMP/$BIN_BASE --version - - - name: Start local reference node for ${{ matrix.runtime }} - run: | - echo Running reference on ${{ matrix.local }} - $TMP/$BIN_REF --chain=${{ matrix.local }} --ws-port=9954 --tmp -- --chain ${{ matrix.relay }} & - sleep 15 - - - name: Start local test node for ${{ matrix.runtime }} - run: | - echo Running test on ${{ matrix.local }} - $TMP/$BIN_BASE --chain=${{ matrix.local }} --ws-port=9944 --tmp -- --chain ${{ matrix.relay }} & - sleep 15 - - - name: Prepare output - run: | - REF_VERSION=$($TMP/$BIN_REF --version) - BIN_VERSION=$($TMP/$BIN_BASE --version) - echo "Metadata comparison:" >> output.txt - echo "Date: $(date)" >> output.txt - echo "Ref. binary: $REF_URL" >> output.txt - echo "Test binary: $BIN_URL" >> output.txt - echo "Ref. version: $REF_VERSION" >> output.txt - echo "Test version: $BIN_VERSION" >> output.txt - echo "Chain: ${{ matrix.local }}" >> output.txt - echo "Relay: ${{ matrix.relay }}" >> output.txt - echo "----------------------------------------------------------------------" >> output.txt - - - name: Pull polkadot-js-tools image - run: docker pull jacogr/polkadot-js-tools - - - name: Compare the metadata - run: | - CMD="docker run --pull always --network host jacogr/polkadot-js-tools metadata ws://localhost:9954 ws://localhost:9944" - echo -e "Running:\n$CMD" - $CMD >> output.txt - sed -z -i 's/\n\n/\n/g' output.txt - cat output.txt | egrep -n -i '' - SUMMARY=$(./scripts/ci/github/extrinsic-ordering-filter.sh output.txt) - echo -e $SUMMARY - echo -e $SUMMARY >> output.txt - - - name: Show result - run: | - cat output.txt - - - name: Save output as artifact - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - with: - name: ${{ matrix.runtime }} - path: | - output.txt - - - name: Stop our local nodes - run: | - pkill $BIN_REF || true - pkill $BIN_BASE || true diff --git a/cumulus/.github/workflows/release-30_create-draft.yml b/cumulus/.github/workflows/release-30_create-draft.yml deleted file mode 100644 index 2d11dfe18cec8..0000000000000 --- a/cumulus/.github/workflows/release-30_create-draft.yml +++ /dev/null @@ -1,311 +0,0 @@ -name: Release - Create draft - -on: - workflow_dispatch: - inputs: - ref1: - description: The 'from' tag to use for the diff - default: parachains-v9.0.0 - required: true - ref2: - description: The 'to' tag to use for the diff - default: release-parachains-v10.0.0 - required: true - release_type: - description: Pass "client" for client releases, leave empty otherwise - required: false - pre_release: - description: For pre-releases - default: "true" - required: true - notification: - description: Whether or not to notify over Matrix - default: "true" - required: true - -jobs: - get-rust-versions: - runs-on: ubuntu-latest - container: - image: paritytech/ci-linux:production - outputs: - rustc-stable: ${{ steps.get-rust-versions.outputs.stable }} - rustc-nightly: ${{ steps.get-rust-versions.outputs.nightly }} - steps: - - id: get-rust-versions - run: | - echo "stable=$(rustc +stable --version)" >> $GITHUB_OUTPUT - echo "nightly=$(rustc +nightly --version)" >> $GITHUB_OUTPUT - - # We do not skip the entire job for client builds (although we don't need it) - # because it is a dep of the next job. However we skip the time consuming steps. - build-runtimes: - runs-on: ubuntu-latest - strategy: - matrix: - include: - - category: assets - runtime: asset-hub-kusama - - category: assets - runtime: asset-hub-polkadot - - category: assets - runtime: asset-hub-westend - - category: bridge-hubs - runtime: bridge-hub-polkadot - - category: bridge-hubs - runtime: bridge-hub-kusama - - category: bridge-hubs - runtime: bridge-hub-rococo - - category: collectives - runtime: collectives-polkadot - - category: contracts - runtime: contracts-rococo - - category: starters - runtime: seedling - - category: starters - runtime: shell - - category: testing - runtime: rococo-parachain - steps: - - name: Checkout sources - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - with: - ref: ${{ github.event.inputs.ref2 }} - - - name: Cache target dir - if: ${{ github.event.inputs.release_type != 'client' }} - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 - with: - path: "${{ github.workspace }}/runtime/${{ matrix.runtime }}/target" - key: srtool-target-${{ matrix.runtime }}-${{ github.sha }} - restore-keys: | - srtool-target-${{ matrix.runtime }}- - srtool-target- - - - name: Build ${{ matrix.runtime }} runtime - if: ${{ github.event.inputs.release_type != 'client' }} - id: srtool_build - uses: chevdor/srtool-actions@v0.7.0 - with: - image: paritytech/srtool - chain: ${{ matrix.runtime }} - runtime_dir: parachains/runtimes/${{ matrix.category }}/${{ matrix.runtime }} - - - name: Store srtool digest to disk - if: ${{ github.event.inputs.release_type != 'client' }} - run: | - echo '${{ steps.srtool_build.outputs.json }}' | \ - jq > ${{ matrix.runtime }}-srtool-digest.json - - - name: Upload ${{ matrix.runtime }} srtool json - if: ${{ github.event.inputs.release_type != 'client' }} - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - with: - name: ${{ matrix.runtime }}-srtool-json - path: ${{ matrix.runtime }}-srtool-digest.json - - - name: Upload ${{ matrix.runtime }} runtime - if: ${{ github.event.inputs.release_type != 'client' }} - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - with: - name: ${{ matrix.runtime }}-runtime - path: | - ${{ steps.srtool_build.outputs.wasm_compressed }} - - publish-draft-release: - runs-on: ubuntu-latest - needs: ["get-rust-versions", "build-runtimes"] - outputs: - release_url: ${{ steps.create-release.outputs.html_url }} - asset_upload_url: ${{ steps.create-release.outputs.upload_url }} - steps: - - name: Checkout sources - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - with: - fetch-depth: 0 - path: cumulus - ref: ${{ github.event.inputs.ref2 }} - - - uses: ruby/setup-ruby@250fcd6a742febb1123a77a841497ccaa8b9e939 # v1.152.0 - with: - ruby-version: 3.0.0 - - - name: Download srtool json output - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 - - - name: Prepare tooling - run: | - cd cumulus/scripts/ci/changelog - gem install bundler changelogerator:0.9.1 - bundle install - changelogerator --help - - URL=https://github.com/chevdor/tera-cli/releases/download/v0.2.1/tera-cli_linux_amd64.deb - wget $URL -O tera.deb - sudo dpkg -i tera.deb - tera --version - - - name: Generate release notes - env: - RUSTC_STABLE: ${{ needs.get-rust-versions.outputs.rustc-stable }} - RUSTC_NIGHTLY: ${{ needs.get-rust-versions.outputs.rustc-nightly }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NO_CACHE: 1 - DEBUG: 1 - SHELL_DIGEST: ${{ github.workspace}}/shell-srtool-json/shell-srtool-digest.json - ASSET_HUB_WESTEND_DIGEST: ${{ github.workspace}}/asset-hub-westend-srtool-json/asset-hub-westend-srtool-digest.json - ASSET_HUB_KUSAMA_DIGEST: ${{ github.workspace}}/asset-hub-kusama-srtool-json/asset-hub-kusama-srtool-digest.json - ASSET_HUB_POLKADOT_DIGEST: ${{ github.workspace}}/asset-hub-polkadot-srtool-json/asset-hub-polkadot-srtool-digest.json - BRIDGE_HUB_ROCOCO_DIGEST: ${{ github.workspace}}/bridge-hub-rococo-srtool-json/bridge-hub-rococo-srtool-digest.json - BRIDGE_HUB_KUSAMA_DIGEST: ${{ github.workspace}}/bridge-hub-kusama-srtool-json/bridge-hub-kusama-srtool-digest.json - BRIDGE_HUB_POLKADOT_DIGEST: ${{ github.workspace}}/bridge-hub-polkadot-srtool-json/bridge-hub-polkadot-srtool-digest.json - COLLECTIVES_POLKADOT_DIGEST: ${{ github.workspace}}/collectives-polkadot-srtool-json/collectives-polkadot-srtool-digest.json - ROCOCO_PARA_DIGEST: ${{ github.workspace}}/rococo-parachain-srtool-json/rococo-parachain-srtool-digest.json - CANVAS_KUSAMA_DIGEST: ${{ github.workspace}}/contracts-rococo-srtool-json/contracts-rococo-srtool-digest.json - REF1: ${{ github.event.inputs.ref1 }} - REF2: ${{ github.event.inputs.ref2 }} - PRE_RELEASE: ${{ github.event.inputs.pre_release }} - RELEASE_TYPE: ${{ github.event.inputs.release_type }} - run: | - find ${{env.GITHUB_WORKSPACE}} -type f -name "*-srtool-digest.json" - - if [ "$RELEASE_TYPE" != "client" ]; then - ls -al $SHELL_DIGEST || true - ls -al $ASSET_HUB_WESTEND_DIGEST || true - ls -al $ASSET_HUB_KUSAMA_DIGEST || true - ls -al $ASSET_HUB_POLKADOT_DIGEST || true - ls -al $BRIDGE_HUB_ROCOCO_DIGEST || true - ls -al $BRIDGE_HUB_KUSAMA_DIGEST || true - ls -al $BRIDGE_HUB_POLKADOT_DIGEST || true - ls -al $COLLECTIVES_POLKADOT_DIGEST || true - ls -al $ROCOCO_PARA_DIGEST || true - ls -al $CANVAS_KUSAMA_DIGEST || true - fi - - echo "The diff will be computed from $REF1 to $REF2" - cd cumulus/scripts/ci/changelog - ./bin/changelog $REF1 $REF2 release-notes.md - ls -al {release-notes.md,context.json} || true - - - name: Archive srtool json - if: ${{ github.event.inputs.release_type != 'client' }} - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - with: - name: srtool-json - path: | - **/*-srtool-digest.json - - - name: Archive context artifact - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - with: - name: release-notes-context - path: | - cumulus/scripts/ci/changelog/context.json - - - name: Create draft release - id: create-release - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - body_path: ./cumulus/scripts/ci/changelog/release-notes.md - tag_name: ${{ github.event.inputs.ref2 }} - release_name: ${{ github.event.inputs.ref2 }} - draft: true - - publish-runtimes: - if: ${{ github.event.inputs.release_type != 'client' }} - runs-on: ubuntu-latest - needs: ["publish-draft-release"] - env: - RUNTIME_DIR: parachains/runtimes - strategy: - matrix: - include: - - category: assets - runtime: asset-hub-kusama - - category: assets - runtime: asset-hub-polkadot - - category: assets - runtime: asset-hub-westend - - category: bridge-hubs - runtime: bridge-hub-polkadot - - category: bridge-hubs - runtime: bridge-hub-kusama - - category: bridge-hubs - runtime: bridge-hub-rococo - - category: collectives - runtime: collectives-polkadot - - category: contracts - runtime: contracts-rococo - - category: starters - runtime: seedling - - category: starters - runtime: shell - - category: testing - runtime: rococo-parachain - steps: - - name: Checkout sources - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - with: - ref: ${{ github.event.inputs.ref2 }} - - - name: Download artifacts - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 - - - uses: ruby/setup-ruby@250fcd6a742febb1123a77a841497ccaa8b9e939 # v1.152.0 - with: - ruby-version: 3.0.0 - - - name: Get runtime version for ${{ matrix.runtime }} - id: get-runtime-ver - run: | - echo "require './scripts/ci/github/runtime-version.rb'" > script.rb - echo "puts get_runtime(runtime: \"${{ matrix.runtime }}\", runtime_dir: \"$RUNTIME_DIR/${{ matrix.category }}\")" >> script.rb - - echo "Current folder: $PWD" - ls "$RUNTIME_DIR/${{ matrix.category }}/${{ matrix.runtime }}" - runtime_ver=$(ruby script.rb) - echo "Found version: >$runtime_ver<" - echo "runtime_ver=$runtime_ver" >> $GITHUB_OUTPUT - - - name: Fix runtime name - id: fix-runtime-path - run: | - cd "${{ matrix.runtime }}-runtime/" - mv "$(sed -E 's/- */_/g' <<< ${{ matrix.runtime }})_runtime.compact.compressed.wasm" "${{ matrix.runtime }}_runtime.compact.compressed.wasm" || true - - - name: Upload compressed ${{ matrix.runtime }} wasm - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.publish-draft-release.outputs.asset_upload_url }} - asset_path: "${{ matrix.runtime }}-runtime/${{ matrix.runtime }}_runtime.compact.compressed.wasm" - asset_name: ${{ matrix.runtime }}_runtime-v${{ steps.get-runtime-ver.outputs.runtime_ver }}.compact.compressed.wasm - asset_content_type: application/wasm - - post_to_matrix: - if: ${{ github.event.inputs.notification == 'true' }} - runs-on: ubuntu-latest - needs: publish-draft-release - strategy: - matrix: - channel: - - name: 'RelEng: Cumulus Release Coordination' - room: '!NAEMyPAHWOiOQHsvus:parity.io' - pre-releases: true - steps: - - name: Matrix notification to ${{ matrix.channel.name }} - uses: s3krit/matrix-message-action@70ad3fb812ee0e45ff8999d6af11cafad11a6ecf # v0.0.3 - with: - room_id: ${{ matrix.channel.room }} - access_token: ${{ secrets.RELEASENOTES_MATRIX_V2_ACCESS_TOKEN }} - server: "m.parity.io" - message: | - **New draft for ${{ github.repository }}**: ${{ github.event.inputs.ref2 }}<br/> - - Draft release created: [draft](${{ needs.publish-draft-release.outputs.release_url }}) - - NOTE: The link above will no longer be valid if the draft is edited. You can then use the following link: - [${{ github.server_url }}/${{ github.repository }}/releases](${{ github.server_url }}/${{ github.repository }}/releases) diff --git a/cumulus/.github/workflows/release-99_bot-announce.yml b/cumulus/.github/workflows/release-99_bot-announce.yml deleted file mode 100644 index 5c2604924c4c0..0000000000000 --- a/cumulus/.github/workflows/release-99_bot-announce.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Release - Pushes release notes to a Matrix room -on: - release: - types: - - published - -jobs: - ping_matrix: - runs-on: ubuntu-latest - strategy: - matrix: - channel: - - name: 'RelEng: Cumulus Release Coordination' - room: '!NAEMyPAHWOiOQHsvus:parity.io' - pre-releases: true - - name: 'Ledger <> Polkadot Coordination' - room: '!EoIhaKfGPmFOBrNSHT:web3.foundation' - pre-release: true - - name: 'General: Rust, Polkadot, Substrate' - room: '!aJymqQYtCjjqImFLSb:parity.io' - pre-release: false - - name: 'Team: DevOps' - room: '!lUslSijLMgNcEKcAiE:parity.io' - pre-release: true - - steps: - - name: Matrix notification to ${{ matrix.channel.name }} - uses: s3krit/matrix-message-action@70ad3fb812ee0e45ff8999d6af11cafad11a6ecf # v0.0.3 - with: - room_id: ${{ matrix.channel.room }} - access_token: ${{ secrets.RELEASENOTES_MATRIX_V2_ACCESS_TOKEN }} - server: "m.parity.io" - message: | - A (pre)release has been ${{github.event.action}} in **${{github.event.repository.full_name}}:**<br/> - Release version: [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) - - ----- - - ${{github.event.release.body}} diff --git a/cumulus/.github/workflows/srtool.yml b/cumulus/.github/workflows/srtool.yml deleted file mode 100644 index ae473b4813709..0000000000000 --- a/cumulus/.github/workflows/srtool.yml +++ /dev/null @@ -1,122 +0,0 @@ -name: Srtool build - -env: - SUBWASM_VERSION: 0.20.0 - -on: - push: - tags: - - "*" - - # paths-ignore: - # - "docker" - # - "docs" - # - "scripts" - # - "test" - # - "client" - paths: - - parachains/runtimes/**/* - - branches: - - "release*" - - schedule: - - cron: "00 02 * * 1" # 2AM weekly on monday - - workflow_dispatch: - -jobs: - srtool: - runs-on: ubuntu-latest - strategy: - matrix: - include: - - category: assets - runtime: asset-hub-kusama - - category: assets - runtime: asset-hub-polkadot - - category: assets - runtime: asset-hub-westend - - category: bridge-hubs - runtime: bridge-hub-polkadot - - category: bridge-hubs - runtime: bridge-hub-kusama - - category: bridge-hubs - runtime: bridge-hub-rococo - - category: collectives - runtime: collectives-polkadot - - category: contracts - runtime: contracts-rococo - - category: starters - runtime: seedling - - category: starters - runtime: shell - - category: testing - runtime: rococo-parachain - steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - with: - fetch-depth: 0 - - - name: Srtool build - id: srtool_build - uses: chevdor/srtool-actions@v0.7.0 - with: - chain: ${{ matrix.runtime }} - runtime_dir: parachains/runtimes/${{ matrix.category }}/${{ matrix.runtime }} - - - name: Summary - run: | - echo '${{ steps.srtool_build.outputs.json }}' | jq > ${{ matrix.runtime }}-srtool-digest.json - cat ${{ matrix.runtime }}-srtool-digest.json - echo "Compact Runtime: ${{ steps.srtool_build.outputs.wasm }}" - echo "Compressed Runtime: ${{ steps.srtool_build.outputs.wasm_compressed }}" - - # it takes a while to build the runtime, so let's save the artifact as soon as we have it - - name: Archive Artifacts for ${{ matrix.runtime }} - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - with: - name: ${{ matrix.runtime }}-runtime - path: | - ${{ steps.srtool_build.outputs.wasm }} - ${{ steps.srtool_build.outputs.wasm_compressed }} - ${{ matrix.runtime }}-srtool-digest.json - - # We now get extra information thanks to subwasm - - name: Install subwasm - run: | - wget https://github.com/chevdor/subwasm/releases/download/v${{ env.SUBWASM_VERSION }}/subwasm_linux_amd64_v${{ env.SUBWASM_VERSION }}.deb - sudo dpkg -i subwasm_linux_amd64_v${{ env.SUBWASM_VERSION }}.deb - subwasm --version - - - name: Show Runtime information - shell: bash - run: | - subwasm info ${{ steps.srtool_build.outputs.wasm }} - subwasm info ${{ steps.srtool_build.outputs.wasm_compressed }} - subwasm --json info ${{ steps.srtool_build.outputs.wasm }} > ${{ matrix.runtime }}-info.json - subwasm --json info ${{ steps.srtool_build.outputs.wasm_compressed }} > ${{ matrix.runtime }}-compressed-info.json - - - name: Extract the metadata - shell: bash - run: | - subwasm meta ${{ steps.srtool_build.outputs.wasm }} - subwasm --json meta ${{ steps.srtool_build.outputs.wasm }} > ${{ matrix.runtime }}-metadata.json - - - name: Check the metadata diff - shell: bash - # the following subwasm call will error for chains that are not known and/or live, that includes shell for instance - run: | - subwasm diff ${{ steps.srtool_build.outputs.wasm }} --chain-b ${{ matrix.runtime }} || \ - echo "Subwasm call failed, check the logs. This is likely because ${{ matrix.runtime }} is not known by subwasm" | \ - tee ${{ matrix.runtime }}-diff.txt - - - name: Archive Subwasm results - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - with: - name: ${{ matrix.runtime }}-runtime - path: | - ${{ matrix.runtime }}-info.json - ${{ matrix.runtime }}-compressed-info.json - ${{ matrix.runtime }}-metadata.json - ${{ matrix.runtime }}-diff.txt diff --git a/cumulus/.gitlab-ci.yml b/cumulus/.gitlab-ci.yml deleted file mode 100644 index f032901c6f471..0000000000000 --- a/cumulus/.gitlab-ci.yml +++ /dev/null @@ -1,201 +0,0 @@ -# .gitlab-ci.yml -# -# cumulus -# -# pipelines can be triggered manually in the web - -stages: - - test - - build - # used for manual job run for regenerate weights for release-* branches (not needed anymore, just leave it here for a while as PlanB) - - benchmarks-build - # used for manual job run for regenerate weights for release-* branches (not needed anymore, just leave it here for a while as PlanB) - - benchmarks-run - - publish - - integration-tests - - zombienet - - short-benchmarks - -default: - interruptible: true - retry: - max: 2 - when: - - runner_system_failure - - unknown_failure - - api_failure - -variables: - GIT_STRATEGY: fetch - GIT_DEPTH: 100 - CARGO_INCREMENTAL: 0 - CI_IMAGE: !reference [.ci-unified, variables, CI_IMAGE] - DOCKER_OS: "debian:stretch" - ARCH: "x86_64" - ZOMBIENET_IMAGE: "docker.io/paritytech/zombienet:v1.3.55" - BUILDAH_IMAGE: "quay.io/buildah/stable:v1.29" - BUILDAH_COMMAND: "buildah --storage-driver overlay2" - -.common-before-script: - before_script: - - !reference [.job-switcher, before_script] - - !reference [.timestamp, before_script] - -.collect-artifacts: - artifacts: - name: "${CI_JOB_NAME}_${CI_COMMIT_REF_NAME}" - when: on_success - expire_in: 1 days - paths: - - ./artifacts/ - -# collecting vars for pipeline stopper -# they will be used if the job fails -.pipeline-stopper-vars: - before_script: - - echo "FAILED_JOB_URL=${CI_JOB_URL}" > pipeline-stopper.env - - echo "FAILED_JOB_NAME=${CI_JOB_NAME}" >> pipeline-stopper.env - - echo "FAILED_JOB_NAME=${CI_JOB_NAME}" >> pipeline-stopper.env - - echo "PR_NUM=${CI_COMMIT_REF_NAME}" >> pipeline-stopper.env - -.pipeline-stopper-artifacts: - artifacts: - reports: - dotenv: pipeline-stopper.env - -.common-refs: - # these jobs run always* - rules: - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_COMMIT_REF_NAME == "master" - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - - if: $CI_COMMIT_REF_NAME =~ /^v[0-9]+\.[0-9]+.*$/ # i.e. v1.0, v2.1rc1 - - if: $CI_COMMIT_REF_NAME =~ /^release-parachains-v[0-9].*$/ # i.e. release-parachains-v1.0, release-parachains-v2.1rc1, release-parachains-v3000 - - if: $CI_COMMIT_REF_NAME =~ /^polkadot-v[0-9]+\.[0-9]+.*$/ # i.e. polkadot-v1.0.99, polkadot-v2.1rc1 - -.pr-refs: - # these jobs run always* - rules: - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - -.publish-refs: - rules: - - if: $CI_COMMIT_REF_NAME == "master" - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_COMMIT_REF_NAME =~ /^v[0-9]+\.[0-9]+.*$/ # i.e. v1.0, v2.1rc1 - -# run benchmarks manually only on release-parachains-v* branch -.benchmarks-manual-refs: - rules: - - if: $CI_COMMIT_REF_NAME =~ /^release-parachains-v[0-9].*$/ # i.e. release-parachains-v1.0, release-parachains-v2.1rc1, release-parachains-v3000 - when: manual - -# run benchmarks only on release-parachains-v* branch -.benchmarks-refs: - rules: - - if: $CI_COMMIT_REF_NAME =~ /^release-parachains-v[0-9].*$/ # i.e. release-parachains-v1.0, release-parachains-v2.1rc1, release-parachains-v3000 - -.zombienet-refs: - rules: - - if: $CI_PIPELINE_SOURCE == "pipeline" - when: never - - if: $CI_PIPELINE_SOURCE == "schedule" - when: never - - if: $CI_COMMIT_REF_NAME == "master" - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - -.job-switcher: - before_script: - - if echo "$CI_DISABLED_JOBS" | grep -xF "$CI_JOB_NAME"; then echo "The job has been cancelled in CI settings"; exit 0; fi - -.docker-env: - image: "${CI_IMAGE}" - before_script: - - !reference [.common-before-script, before_script] - - rustup show - - cargo --version - - bash --version - tags: - - linux-docker-vm-c2 - -.kubernetes-env: - image: "${CI_IMAGE}" - before_script: - - !reference [.common-before-script, before_script] - tags: - - kubernetes-parity-build - -.git-commit-push: - script: - - git status - # Set git config - - rm -rf .git/config - - git config --global user.email "${GITHUB_EMAIL}" - - git config --global user.name "${GITHUB_USER}" - - git config remote.origin.url "https://${GITHUB_USER}:${GITHUB_TOKEN}@github.com/paritytech/${CI_PROJECT_NAME}.git" - - git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" - # push results to github - - git checkout -b $BRANCHNAME - - git add parachains/* - - git commit -m "[benchmarks] pr with weights" - - git push origin $BRANCHNAME - -include: - # test jobs - - scripts/ci/gitlab/pipeline/test.yml - # # build jobs - - scripts/ci/gitlab/pipeline/build.yml - # short-benchmarks jobs - - scripts/ci/gitlab/pipeline/short-benchmarks.yml - # # benchmarks jobs - # # used for manual job run for regenerate weights for release-* branches (not needed anymore, just leave it here for a while as PlanB) - - scripts/ci/gitlab/pipeline/benchmarks.yml - # # publish jobs - - scripts/ci/gitlab/pipeline/publish.yml - # zombienet jobs - - scripts/ci/gitlab/pipeline/zombienet.yml - # timestamp handler - - project: parity/infrastructure/ci_cd/shared - ref: main - file: /common/timestamp.yml - - project: parity/infrastructure/ci_cd/shared - ref: main - file: /common/ci-unified.yml - - -#### stage: .post - -# This job cancels the whole pipeline if any of provided jobs fail. -# In a DAG, every jobs chain is executed independently of others. The `fail_fast` principle suggests -# to fail the pipeline as soon as possible to shorten the feedback loop. -cancel-pipeline: - stage: .post - needs: - - job: test-linux-stable - rules: - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - when: on_failure - variables: - PROJECT_ID: "${CI_PROJECT_ID}" - PROJECT_NAME: "${CI_PROJECT_NAME}" - PIPELINE_ID: "${CI_PIPELINE_ID}" - FAILED_JOB_URL: "${FAILED_JOB_URL}" - FAILED_JOB_NAME: "${FAILED_JOB_NAME}" - PR_NUM: "${PR_NUM}" - trigger: - project: "parity/infrastructure/ci_cd/pipeline-stopper" - branch: "as-improve" - -remove-cancel-pipeline-message: - stage: .post - rules: - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - variables: - PROJECT_ID: "${CI_PROJECT_ID}" - PROJECT_NAME: "${CI_PROJECT_NAME}" - PIPELINE_ID: "${CI_PIPELINE_ID}" - FAILED_JOB_URL: "https://gitlab.com" - FAILED_JOB_NAME: "nope" - PR_NUM: "${CI_COMMIT_REF_NAME}" - trigger: - project: "parity/infrastructure/ci_cd/pipeline-stopper" diff --git a/cumulus/scripts/ci/changelog/README.md b/cumulus/scripts/ci/changelog/README.md deleted file mode 100644 index 5c8ee9c9b914e..0000000000000 --- a/cumulus/scripts/ci/changelog/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# Changelog - -Currently, the changelog is built locally. It will be moved to CI once labels stabilize. - -For now, a bit of preparation is required before you can run the script: -- fetch the srtool digests -- store them under the `digests` folder as `<chain>-srtool-digest.json` -- ensure the `.env` file is up to date with correct information - -The content of the release notes is generated from the template files under the `scripts/ci/changelog/templates` folder. -For readability and maintenance, the template is split into several small snippets. - -Run: -``` -./bin/changelog <ref_since> [<ref_until>=HEAD] -``` - -For instance: -``` -./bin/changelog parachains-v7.0.0-rc8 -``` - -A file called `release-notes.md` will be generated and can be used for the release. - -## ENV - -You may use the following ENV for testing: - -``` -RUSTC_STABLE="rustc 1.56.1 (59eed8a2a 2021-11-01)" -RUSTC_NIGHTLY="rustc 1.57.0-nightly (51e514c0f 2021-09-12)" -PRE_RELEASE=true -HIDE_SRTOOL_ROCOCO=true -HIDE_SRTOOL_SHELL=true -REF1=statemine-v5.0.0 -REF2=HEAD -DEBUG=1 -NO_CACHE=1 -``` - -By default, the template will include all the information, including the runtime data. For clients releases, we don't -need those and they can be skipped by setting the following env: -``` -RELEASE_TYPE=client -``` - -## Considered labels - -The following list will likely evolve over time and it will be hard to keep it in sync. In any case, if you want to find -all the labels that are used, search for `meta` in the templates. Currently, the considered labels are: - -- Priority: C<N> labels -- Audit: D<N> labels -- E4 => new host function -- B0 => silent, not showing up -- B1-releasenotes (misc unless other labels) -- B5-client (client changes) -- B7-runtimenoteworthy (runtime changes) -- T6-XCM - -Note that labels with the same letter are mutually exclusive. A PR should not have both `B0` and `B5`, or both `C1` and -`C9`. In case of conflicts, the template will decide which label will be considered. - -## Dev and debugging - -### Hot Reload - -The following command allows **Hot Reload**: -``` -fswatch templates -e ".*\.md$" | xargs -n1 -I{} ./bin/changelog statemine-v5.0.0 -``` -### Caching - -By default, if the changelog data from Github is already present, the calls to the Github API will be skipped and the -local version of the data will be used. This is much faster. If you know that some labels have changed in Github, you -probably want to refresh the data. You can then either delete manually the `cumulus.json` file or `export NO_CACHE=1` to -force refreshing the data. diff --git a/polkadot/.gitattributes b/polkadot/.gitattributes deleted file mode 100644 index 2ea1ab2d6b9cf..0000000000000 --- a/polkadot/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -/.gitlab-ci.yml filter=ci-prettier -/scripts/ci/gitlab/pipeline/*.yml filter=ci-prettier diff --git a/polkadot/.github/dependabot.yml b/polkadot/.github/dependabot.yml deleted file mode 100644 index a1fa925970bbb..0000000000000 --- a/polkadot/.github/dependabot.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "cargo" - directory: "/" - labels: ["A2-insubstantial", "B0-silent", "C1-low", "E2-dependencies"] - # Handle updates for crates from github.com/paritytech/substrate manually. - ignore: - - dependency-name: "substrate-*" - - dependency-name: "sc-*" - - dependency-name: "sp-*" - - dependency-name: "frame-*" - - dependency-name: "fork-tree" - - dependency-name: "frame-remote-externalities" - - dependency-name: "pallet-*" - - dependency-name: "beefy-*" - - dependency-name: "try-runtime-*" - - dependency-name: "test-runner" - - dependency-name: "generate-bags" - - dependency-name: "sub-tokens" - schedule: - interval: "daily" - - package-ecosystem: github-actions - directory: '/' - labels: ["A2-insubstantial", "B0-silent", "C1-low", "E2-dependencies"] - schedule: - interval: daily diff --git a/polkadot/.github/pr-custom-review.yml b/polkadot/.github/pr-custom-review.yml deleted file mode 100644 index 136c9e75ff2de..0000000000000 --- a/polkadot/.github/pr-custom-review.yml +++ /dev/null @@ -1,42 +0,0 @@ -# 🔒 PROTECTED: Changes to locks-review-team should be approved by the current locks-review-team -locks-review-team: locks-review -team-leads-team: polkadot-review -action-review-team: ci - -rules: - - name: Runtime files - check_type: changed_files - condition: - include: ^runtime\/(kusama|polkadot)\/src\/.+\.rs$ - exclude: ^runtime\/(kusama|polkadot)\/src\/weights\/.+\.rs$ - all_distinct: - - min_approvals: 1 - teams: - - locks-review - - min_approvals: 1 - teams: - - polkadot-review - - - name: Core developers - check_type: changed_files - condition: - include: .* - # excluding files from 'Runtime files' and 'CI files' rules - exclude: ^runtime/(kusama|polkadot)/src/[^/]+\.rs$|^\.gitlab-ci\.yml|^(?!.*\.dic$|.*spellcheck\.toml$)scripts/ci/.*|^\.github/.* - min_approvals: 3 - teams: - - core-devs - - - name: CI files - check_type: changed_files - condition: - # dictionary files are excluded - include: ^\.gitlab-ci\.yml|^(?!.*\.dic$|.*spellcheck\.toml$)scripts/ci/.*|^\.github/.* - min_approvals: 2 - teams: - - ci - - release-engineering - -prevent-review-request: - teams: - - core-devs diff --git a/polkadot/.github/workflows/burnin-label-notification.yml b/polkadot/.github/workflows/burnin-label-notification.yml deleted file mode 100644 index 536f8fa2a3f65..0000000000000 --- a/polkadot/.github/workflows/burnin-label-notification.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Notify devops when burn-in label applied -on: - pull_request: - types: [labeled] - -jobs: - notify-devops: - runs-on: ubuntu-latest - strategy: - matrix: - channel: - - name: 'Team: DevOps' - room: '!lUslSijLMgNcEKcAiE:parity.io' - - steps: - - name: Send Matrix message to ${{ matrix.channel.name }} - if: startsWith(github.event.label.name, 'A1-') - uses: s3krit/matrix-message-action@70ad3fb812ee0e45ff8999d6af11cafad11a6ecf # v0.0.3 - with: - room_id: ${{ matrix.channel.room }} - access_token: ${{ secrets.RELEASENOTES_MATRIX_V2_ACCESS_TOKEN }} - server: m.parity.io - message: | - @room Burn-in request received for the following PR: ${{ github.event.pull_request.html_url }} diff --git a/polkadot/.github/workflows/check-D-labels.yml b/polkadot/.github/workflows/check-D-labels.yml deleted file mode 100644 index 9abefaa6fa100..0000000000000 --- a/polkadot/.github/workflows/check-D-labels.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Check D labels - -on: - pull_request: - types: [labeled, opened, synchronize, unlabeled] - paths: - - runtime/polkadot/** - - runtime/kusama/** - - runtime/common/** - - primitives/src/** - -jobs: - check-labels: - runs-on: ubuntu-latest - steps: - - name: Pull image - env: - IMAGE: paritytech/ruled_labels:0.4.0 - run: docker pull $IMAGE - - - name: Check labels - env: - IMAGE: paritytech/ruled_labels:0.4.0 - MOUNT: /work - GITHUB_PR: ${{ github.event.pull_request.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - API_BASE: https://api.github.com/repos - REPO: ${{ github.repository }} - RULES_PATH: labels/ruled_labels - CHECK_SPECS: specs_polkadot.yaml - run: | - echo "REPO: ${REPO}" - echo "GITHUB_PR: ${GITHUB_PR}" - # Clone repo with labels specs - git clone https://github.com/paritytech/labels - # Fetch the labels for the PR under test - labels=$( curl -H "Authorization: token ${GITHUB_TOKEN}" -s "$API_BASE/${REPO}/pulls/${GITHUB_PR}" | jq '.labels | .[] | .name' | tr "\n" ",") - - if [ -z "${labels}" ]; then - docker run --rm -i -v $PWD/${RULES_PATH}/:$MOUNT $IMAGE check $MOUNT/$CHECK_SPECS --tags audit --no-label - fi - - labels_args=${labels: :-1} - printf "Checking labels: %s\n" "${labels_args}" - - # Prevent the shell from splitting labels with spaces - IFS="," - - # --dev is more useful to debug mode to debug - docker run --rm -i -v $PWD/${RULES_PATH}/:$MOUNT $IMAGE check $MOUNT/$CHECK_SPECS --labels ${labels_args} --dev --tags audit diff --git a/polkadot/.github/workflows/check-bootnodes.yml b/polkadot/.github/workflows/check-bootnodes.yml deleted file mode 100644 index 897a90d3ae928..0000000000000 --- a/polkadot/.github/workflows/check-bootnodes.yml +++ /dev/null @@ -1,31 +0,0 @@ -# checks all networks we care about (kusama, polkadot, westend) and ensures -# the bootnodes in their respective chainspecs are contactable - -name: Check all bootnodes -on: - push: - branches: - # Catches v1.2.3 and v1.2.3-rc1 - - release-v[0-9]+.[0-9]+.[0-9]+* - -jobs: - check_bootnodes: - strategy: - fail-fast: false - matrix: - runtime: [westend, kusama, polkadot] - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - name: Install polkadot - shell: bash - run: | - curl -L "$(curl -s https://api.github.com/repos/paritytech/polkadot/releases/latest \ - | jq -r '.assets | .[] | select(.name == "polkadot").browser_download_url')" \ - | sudo tee /usr/local/bin/polkadot > /dev/null - sudo chmod +x /usr/local/bin/polkadot - polkadot --version - - name: Check ${{ matrix.runtime }} bootnodes - shell: bash - run: scripts/ci/github/check_bootnodes.sh node/service/chain-specs/${{ matrix.runtime }}.json diff --git a/polkadot/.github/workflows/check-labels.yml b/polkadot/.github/workflows/check-labels.yml deleted file mode 100644 index df0a0e9cf02dd..0000000000000 --- a/polkadot/.github/workflows/check-labels.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Check labels - -on: - pull_request: - types: [labeled, opened, synchronize, unlabeled] - -jobs: - check-labels: - runs-on: ubuntu-latest - steps: - - name: Pull image - env: - IMAGE: paritytech/ruled_labels:0.4.0 - run: docker pull $IMAGE - - - name: Check labels - env: - IMAGE: paritytech/ruled_labels:0.4.0 - MOUNT: /work - GITHUB_PR: ${{ github.event.pull_request.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - API_BASE: https://api.github.com/repos - REPO: ${{ github.repository }} - RULES_PATH: labels/ruled_labels - CHECK_SPECS: specs_polkadot.yaml - run: | - echo "REPO: ${REPO}" - echo "GITHUB_PR: ${GITHUB_PR}" - # Clone repo with labels specs - git clone https://github.com/paritytech/labels - # Fetch the labels for the PR under test - labels=$( curl -H "Authorization: token ${GITHUB_TOKEN}" -s "$API_BASE/${REPO}/pulls/${GITHUB_PR}" | jq '.labels | .[] | .name' | tr "\n" ",") - - if [ -z "${labels}" ]; then - docker run --rm -i -v $PWD/${RULES_PATH}/:$MOUNT $IMAGE check $MOUNT/$CHECK_SPECS --tags PR --no-label - fi - - labels_args=${labels: :-1} - printf "Checking labels: %s\n" "${labels_args}" - - # Prevent the shell from splitting labels with spaces - IFS="," - - # --dev is more useful to debug mode to debug - docker run --rm -i -v $PWD/${RULES_PATH}/:$MOUNT $IMAGE check $MOUNT/$CHECK_SPECS --labels ${labels_args} --dev --tags PR diff --git a/polkadot/.github/workflows/check-licenses.yml b/polkadot/.github/workflows/check-licenses.yml deleted file mode 100644 index 1e654f7b30705..0000000000000 --- a/polkadot/.github/workflows/check-licenses.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Check licenses - -on: - pull_request: - -jobs: - check-licenses: - runs-on: ubuntu-22.04 - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - uses: actions/setup-node@v3.8.1 - with: - node-version: '18.x' - registry-url: 'https://npm.pkg.github.com' - scope: '@paritytech' - - name: Check the licenses - run: | - shopt -s globstar - - npx @paritytech/license-scanner@0.0.5 scan \ - --ensure-licenses=Apache-2.0 \ - --ensure-licenses=GPL-3.0-only \ - ./**/*.rs - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/polkadot/.github/workflows/check-new-bootnodes.yml b/polkadot/.github/workflows/check-new-bootnodes.yml deleted file mode 100644 index 25b2a0a56fe5f..0000000000000 --- a/polkadot/.github/workflows/check-new-bootnodes.yml +++ /dev/null @@ -1,28 +0,0 @@ -# If a chainspec file is updated with new bootnodes, we check to make sure those bootnodes are contactable - -name: Check new bootnodes -on: - pull_request: - paths: - - 'node/service/chain-specs/*.json' - -jobs: - check_bootnodes: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Install polkadot - shell: bash - run: | - curl -L "$(curl -s https://api.github.com/repos/paritytech/polkadot/releases/latest \ - | jq -r '.assets | .[] | select(.name == "polkadot").browser_download_url')" \ - | sudo tee /usr/local/bin/polkadot > /dev/null - sudo chmod +x /usr/local/bin/polkadot - polkadot --version - - name: Check new bootnodes - shell: bash - run: | - scripts/ci/github/check_new_bootnodes.sh diff --git a/polkadot/.github/workflows/check-weights.yml b/polkadot/.github/workflows/check-weights.yml deleted file mode 100644 index e6a6c43e0a6a9..0000000000000 --- a/polkadot/.github/workflows/check-weights.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Check updated weights - -on: - pull_request: - paths: - - 'runtime/*/src/weights/**' - -jobs: - check_weights_files: - strategy: - fail-fast: false - matrix: - runtime: [westend, kusama, polkadot, rococo] - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - name: Check weights files - shell: bash - run: | - scripts/ci/github/verify_updated_weights.sh ${{ matrix.runtime }} - - # This job uses https://github.com/ggwpez/substrate-weight-compare to compare the weights of the current - # release with the last release, then adds them as a comment to the PR. - check_weight_changes: - strategy: - fail-fast: false - matrix: - runtime: [westend, kusama, polkadot, rococo] - runs-on: ubuntu-latest - steps: - - name: Get latest release - run: | - LAST_RELEASE=$(curl -s https://api.github.com/repos/paritytech/polkadot/releases/latest | jq -r .tag_name) - echo "LAST_RELEASE=$LAST_RELEASE" >> $GITHUB_ENV - - name: Checkout current sources - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Check weight changes - shell: bash - run: | - cargo install --git https://github.com/ggwpez/substrate-weight-compare swc - ./scripts/ci/github/check_weights_swc.sh ${{ matrix.runtime }} "$LAST_RELEASE" | tee swc_output_${{ matrix.runtime }}.md - - name: Add comment - uses: thollander/actions-comment-pull-request@v2 - with: - filePath: ./swc_output_${{ matrix.runtime }}.md - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/polkadot/.github/workflows/honggfuzz.yml b/polkadot/.github/workflows/honggfuzz.yml deleted file mode 100644 index 27fa0d9967f3b..0000000000000 --- a/polkadot/.github/workflows/honggfuzz.yml +++ /dev/null @@ -1,137 +0,0 @@ -name: Run nightly fuzzer jobs - -on: - schedule: - - cron: '0 0 * * *' - -jobs: - xcm-fuzzer: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - fetch-depth: 1 - - - name: Install minimal stable Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - name: Install minimal nightly Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - target: wasm32-unknown-unknown - - - name: Install honggfuzz deps - run: sudo apt-get install --no-install-recommends binutils-dev libunwind8-dev - - - name: Install honggfuzz - uses: actions-rs/cargo@v1 - with: - command: install - args: honggfuzz --version "0.5.54" - - - name: Build fuzzer binaries - working-directory: xcm/xcm-simulator/fuzzer/ - run: cargo hfuzz build - - - name: Run fuzzer - working-directory: xcm/xcm-simulator/fuzzer/ - run: bash $GITHUB_WORKSPACE/scripts/ci/github/run_fuzzer.sh xcm-fuzzer - - erasure-coding-round-trip: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - fetch-depth: 1 - - - name: Cache Seed - id: cache-seed-round-trip - uses: actions/cache@v3 - with: - path: erasure-coding/fuzzer/hfuzz_workspace - key: ${{ runner.os }}-erasure-coding - - - name: Install minimal stable Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - name: Install minimal nightly Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - target: wasm32-unknown-unknown - - - name: Install honggfuzz deps - run: sudo apt-get install --no-install-recommends binutils-dev libunwind8-dev - - - name: Install honggfuzz - uses: actions-rs/cargo@v1 - with: - command: install - args: honggfuzz --version "0.5.54" - - - name: Build fuzzer binaries - working-directory: erasure-coding/fuzzer - run: cargo hfuzz build - - - name: Run fuzzer - working-directory: erasure-coding/fuzzer - run: bash $GITHUB_WORKSPACE/scripts/ci/github/run_fuzzer.sh round_trip - - erasure-coding-reconstruct: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - fetch-depth: 1 - - - name: Cache Seed - id: cache-seed-reconstruct - uses: actions/cache@v3 - with: - path: erasure-coding/fuzzer/hfuzz_workspace - key: ${{ runner.os }}-erasure-coding - - - name: Install minimal stable Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - name: Install minimal nightly Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - target: wasm32-unknown-unknown - - - name: Install honggfuzz deps - run: sudo apt-get install --no-install-recommends binutils-dev libunwind8-dev - - - name: Install honggfuzz - uses: actions-rs/cargo@v1 - with: - command: install - args: honggfuzz --version "0.5.54" - - - name: Build fuzzer binaries - working-directory: erasure-coding/fuzzer - run: cargo hfuzz build - - - name: Run fuzzer - working-directory: erasure-coding/fuzzer - run: bash $GITHUB_WORKSPACE/scripts/ci/github/run_fuzzer.sh reconstruct diff --git a/polkadot/.github/workflows/pr-custom-review.yml b/polkadot/.github/workflows/pr-custom-review.yml deleted file mode 100644 index 8e40c9ee72989..0000000000000 --- a/polkadot/.github/workflows/pr-custom-review.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Assign reviewers - -on: - pull_request: - branches: - - master - - main - types: - - opened - - reopened - - synchronize - - review_requested - - review_request_removed - - ready_for_review - - converted_to_draft - pull_request_review: - -jobs: - pr-custom-review: - runs-on: ubuntu-latest - steps: - - name: Skip if pull request is in Draft - # `if: github.event.pull_request.draft == true` should be kept here, at - # the step level, rather than at the job level. The latter is not - # recommended because when the PR is moved from "Draft" to "Ready to - # review" the workflow will immediately be passing (since it was skipped), - # even though it hasn't actually ran, since it takes a few seconds for - # the workflow to start. This is also disclosed in: - # https://github.community/t/dont-run-actions-on-draft-pull-requests/16817/17 - # That scenario would open an opportunity for the check to be bypassed: - # 1. Get your PR approved - # 2. Move it to Draft - # 3. Push whatever commits you want - # 4. Move it to "Ready for review"; now the workflow is passing (it was - # skipped) and "Check reviews" is also passing (it won't be updated - # until the workflow is finished) - if: github.event.pull_request.draft == true - run: exit 1 - - name: pr-custom-review - uses: paritytech/pr-custom-review@action-v3 - with: - checks-reviews-api: http://pcr.parity-prod.parity.io/api/v1/check_reviews diff --git a/polkadot/.github/workflows/release-01_branch-check.yml b/polkadot/.github/workflows/release-01_branch-check.yml deleted file mode 100644 index f2b559b7c176b..0000000000000 --- a/polkadot/.github/workflows/release-01_branch-check.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Release - Branch check -on: - push: - branches: - # Catches v1.2.3 and v1.2.3-rc1 - - release-v[0-9]+.[0-9]+.[0-9]+* - - workflow_dispatch: - -jobs: - check_branch: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Run check - shell: bash - run: ./scripts/ci/github/check-rel-br diff --git a/polkadot/.github/workflows/release-10_candidate.yml b/polkadot/.github/workflows/release-10_candidate.yml deleted file mode 100644 index 54a937a7819a1..0000000000000 --- a/polkadot/.github/workflows/release-10_candidate.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Release - RC automation -on: - push: - branches: - # Catches v1.2.3 and v1.2.3-rc1 - - release-v[0-9]+.[0-9]+.[0-9]+* -jobs: - tag_rc: - runs-on: ubuntu-latest - strategy: - matrix: - channel: - - name: "RelEng: Polkadot Release Coordination" - room: '!cqAmzdIcbOFwrdrubV:parity.io' - - steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - id: compute_tag - name: Compute next rc tag - shell: bash - run: | - # Get last rc tag if exists, else set it to {version}-rc1 - version=${GITHUB_REF#refs/heads/release-} - echo "$version" - echo "version=$version" >> $GITHUB_OUTPUT - git tag -l - last_rc=$(git tag -l "$version-rc*" | sort -V | tail -n 1) - if [ -n "$last_rc" ]; then - suffix=$(echo "$last_rc" | grep -Eo '[0-9]+$') - echo $suffix - ((suffix++)) - echo $suffix - echo "new_tag=$version-rc$suffix" >> $GITHUB_OUTPUT - echo "first_rc=false" >> $GITHUB_OUTPUT - else - echo "new_tag=$version-rc1" >> $GITHUB_OUTPUT - echo "first_rc=true" >> $GITHUB_OUTPUT - fi - - - name: Apply new tag - uses: tvdias/github-tagger@ed7350546e3e503b5e942dffd65bc8751a95e49d # v0.0.2 - with: - # We can't use the normal GITHUB_TOKEN for the following reason: - # https://docs.github.com/en/actions/reference/events-that-trigger-workflows#triggering-new-workflows-using-a-personal-access-token - # RELEASE_BRANCH_TOKEN requires public_repo OAuth scope - repo-token: "${{ secrets.RELEASE_BRANCH_TOKEN }}" - tag: ${{ steps.compute_tag.outputs.new_tag }} - - - id: create-issue - uses: JasonEtco/create-an-issue@e27dddc79c92bc6e4562f268fffa5ed752639abd # v2.9.1 - # Only create the issue if it's the first release candidate - if: steps.compute_tag.outputs.first_rc == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.compute_tag.outputs.version }} - with: - filename: .github/ISSUE_TEMPLATE/release.md - - - name: Send Matrix message to ${{ matrix.channel.name }} - uses: s3krit/matrix-message-action@70ad3fb812ee0e45ff8999d6af11cafad11a6ecf # v0.0.3 - if: steps.create-issue.outputs.url != '' - with: - room_id: ${{ matrix.channel.room }} - access_token: ${{ secrets.RELEASENOTES_MATRIX_V2_ACCESS_TOKEN }} - server: m.parity.io - message: | - Release process for polkadot ${{ steps.compute_tag.outputs.version }} has been started.<br/> - Tracking issue: ${{ steps.create-issue.outputs.url }} diff --git a/polkadot/.github/workflows/release-20_extrinsic-ordering-check-from-bin.yml b/polkadot/.github/workflows/release-20_extrinsic-ordering-check-from-bin.yml deleted file mode 100644 index 0613ed04d35a9..0000000000000 --- a/polkadot/.github/workflows/release-20_extrinsic-ordering-check-from-bin.yml +++ /dev/null @@ -1,81 +0,0 @@ -# This workflow performs the Extrinsic Ordering Check on demand using a binary - -name: Release - Extrinsic Ordering Check -on: - workflow_dispatch: - inputs: - reference_url: - description: The WebSocket url of the reference node - default: wss://kusama-rpc.polkadot.io - required: true - binary_url: - description: A url to a Linux binary for the node containing the runtime to test - default: https://releases.parity.io/polkadot/x86_64-debian:stretch/v0.9.10/polkadot - required: true - chain: - description: The name of the chain under test. Usually, you would pass a local chain - default: kusama-local - required: true - -jobs: - check: - name: Run check - runs-on: ubuntu-latest - env: - CHAIN: ${{github.event.inputs.chain}} - BIN_URL: ${{github.event.inputs.binary_url}} - REF_URL: ${{github.event.inputs.reference_url}} - - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - - name: Fetch binary - run: | - echo Fetching $BIN_URL - wget $BIN_URL - chmod a+x polkadot - ./polkadot --version - - - name: Start local node - run: | - echo Running on $CHAIN - ./polkadot --chain=$CHAIN & - - - name: Prepare output - run: | - VERSION=$(./polkadot --version) - echo "Metadata comparison:" >> output.txt - echo "Date: $(date)" >> output.txt - echo "Reference: $REF_URL" >> output.txt - echo "Target version: $VERSION" >> output.txt - echo "Chain: $CHAIN" >> output.txt - echo "----------------------------------------------------------------------" >> output.txt - - - name: Pull polkadot-js-tools image - run: docker pull jacogr/polkadot-js-tools - - - name: Compare the metadata - run: | - CMD="docker run --pull always --network host jacogr/polkadot-js-tools metadata $REF_URL ws://localhost:9944" - echo -e "Running:\n$CMD" - $CMD >> output.txt - sed -z -i 's/\n\n/\n/g' output.txt - cat output.txt | egrep -n -i '' - SUMMARY=$(./scripts/ci/github/extrinsic-ordering-filter.sh output.txt) - echo -e $SUMMARY - echo -e $SUMMARY >> output.txt - - - name: Show result - run: | - cat output.txt - - - name: Stop our local node - run: pkill polkadot - - - name: Save output as artifact - uses: actions/upload-artifact@v3 - with: - name: ${{ env.CHAIN }} - path: | - output.txt diff --git a/polkadot/.github/workflows/release-21_extrinsic-ordering-check-from-two.yml b/polkadot/.github/workflows/release-21_extrinsic-ordering-check-from-two.yml deleted file mode 100644 index 6513897f4a134..0000000000000 --- a/polkadot/.github/workflows/release-21_extrinsic-ordering-check-from-two.yml +++ /dev/null @@ -1,97 +0,0 @@ -# This workflow performs the Extrinsic Ordering Check on demand using two reference binaries - -name: Release - Extrinsic API Check with reference bins -on: - workflow_dispatch: - inputs: - reference_binary_url: - description: A url to a Linux binary for the node containing the reference runtime to test against - default: https://releases.parity.io/polkadot/x86_64-debian:stretch/v0.9.26/polkadot - required: true - binary_url: - description: A url to a Linux binary for the node containing the runtime to test - default: https://releases.parity.io/polkadot/x86_64-debian:stretch/v0.9.27-rc1/polkadot - required: true - -jobs: - check: - name: Run check - runs-on: ubuntu-latest - env: - BIN_URL: ${{github.event.inputs.binary_url}} - REF_URL: ${{github.event.inputs.reference_binary_url}} - strategy: - fail-fast: false - matrix: - chain: [polkadot, kusama, westend, rococo] - - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - - name: Fetch reference binary - run: | - echo Fetching $REF_URL - curl $REF_URL -o polkadot-ref - chmod a+x polkadot-ref - ./polkadot-ref --version - - - name: Fetch test binary - run: | - echo Fetching $BIN_URL - curl $BIN_URL -o polkadot - chmod a+x polkadot - ./polkadot --version - - - name: Start local reference node - run: | - echo Running reference on ${{ matrix.chain }}-local - ./polkadot-ref --chain=${{ matrix.chain }}-local --rpc-port=9934 --ws-port=9945 --base-path=polkadot-ref-base/ & - - - name: Start local test node - run: | - echo Running test on ${{ matrix.chain }}-local - ./polkadot --chain=${{ matrix.chain }}-local & - - - name: Prepare output - run: | - REF_VERSION=$(./polkadot-ref --version) - BIN_VERSION=$(./polkadot --version) - echo "Metadata comparison:" >> output.txt - echo "Date: $(date)" >> output.txt - echo "Ref. binary: $REF_URL" >> output.txt - echo "Test binary: $BIN_URL" >> output.txt - echo "Ref. version: $REF_VERSION" >> output.txt - echo "Test version: $BIN_VERSION" >> output.txt - echo "Chain: ${{ matrix.chain }}-local" >> output.txt - echo "----------------------------------------------------------------------" >> output.txt - - - name: Pull polkadot-js-tools image - run: docker pull jacogr/polkadot-js-tools - - - name: Compare the metadata - run: | - CMD="docker run --pull always --network host jacogr/polkadot-js-tools metadata ws://localhost:9945 ws://localhost:9944" - echo -e "Running:\n$CMD" - $CMD >> output.txt - sed -z -i 's/\n\n/\n/g' output.txt - cat output.txt | egrep -n -i '' - SUMMARY=$(./scripts/ci/github/extrinsic-ordering-filter.sh output.txt) - echo -e $SUMMARY - echo -e $SUMMARY >> output.txt - - - name: Show result - run: | - cat output.txt - - - name: Save output as artifact - uses: actions/upload-artifact@v3 - with: - name: ${{ matrix.chain }} - path: | - output.txt - - - name: Stop our local nodes - run: | - pkill polkadot-ref - pkill polkadot diff --git a/polkadot/.github/workflows/release-30_publish-draft-release.yml b/polkadot/.github/workflows/release-30_publish-draft-release.yml deleted file mode 100644 index 206b1871d80a4..0000000000000 --- a/polkadot/.github/workflows/release-30_publish-draft-release.yml +++ /dev/null @@ -1,199 +0,0 @@ -name: Release - Publish draft - -on: - push: - tags: - # Catches v1.2.3 and v1.2.3-rc1 - - v[0-9]+.[0-9]+.[0-9]+* - -jobs: - get-rust-versions: - runs-on: ubuntu-latest - container: - image: paritytech/ci-linux:production - outputs: - rustc-stable: ${{ steps.get-rust-versions.outputs.stable }} - rustc-nightly: ${{ steps.get-rust-versions.outputs.nightly }} - steps: - - id: get-rust-versions - run: | - echo "stable=$(rustc +stable --version)" >> $GITHUB_OUTPUT - echo "nightly=$(rustc +nightly --version)" >> $GITHUB_OUTPUT - - build-runtimes: - runs-on: ubuntu-latest - strategy: - matrix: - runtime: ["polkadot", "kusama", "westend", "rococo"] - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - - name: Cache target dir - uses: actions/cache@v3 - with: - path: "${{ github.workspace }}/runtime/${{ matrix.runtime }}/target" - key: srtool-target-${{ matrix.runtime }}-${{ github.sha }} - restore-keys: | - srtool-target-${{ matrix.runtime }}- - srtool-target- - - - name: Build ${{ matrix.runtime }} runtime - id: srtool_build - uses: chevdor/srtool-actions@v0.8.0 - with: - image: paritytech/srtool - chain: ${{ matrix.runtime }} - - - name: Store srtool digest to disk - run: | - echo '${{ steps.srtool_build.outputs.json }}' | jq > ${{ matrix.runtime }}_srtool_output.json - - - name: Upload ${{ matrix.runtime }} srtool json - uses: actions/upload-artifact@v3 - with: - name: ${{ matrix.runtime }}-srtool-json - path: ${{ matrix.runtime }}_srtool_output.json - - - name: Upload ${{ matrix.runtime }} runtime - uses: actions/upload-artifact@v3 - with: - name: ${{ matrix.runtime }}-runtime - path: | - ${{ steps.srtool_build.outputs.wasm_compressed }} - - publish-draft-release: - runs-on: ubuntu-latest - needs: ["get-rust-versions", "build-runtimes"] - outputs: - release_url: ${{ steps.create-release.outputs.html_url }} - asset_upload_url: ${{ steps.create-release.outputs.upload_url }} - steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - fetch-depth: 0 - path: polkadot - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.0.0 - - - name: Download srtool json output - uses: actions/download-artifact@v3 - - - name: Prepare tooling - run: | - cd polkadot/scripts/ci/changelog - gem install bundler changelogerator:0.9.1 - bundle install - changelogerator --help - - URL=https://github.com/chevdor/tera-cli/releases/download/v0.2.1/tera-cli_linux_amd64.deb - wget $URL -O tera.deb - sudo dpkg -i tera.deb - tera --version - - - name: Generate release notes - env: - RUSTC_STABLE: ${{ needs.get-rust-versions.outputs.rustc-stable }} - RUSTC_NIGHTLY: ${{ needs.get-rust-versions.outputs.rustc-nightly }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NO_CACHE: 1 - DEBUG: 1 - ROCOCO_DIGEST: ${{ github.workspace}}/rococo-srtool-json/rococo_srtool_output.json - WESTEND_DIGEST: ${{ github.workspace}}/westend-srtool-json/westend_srtool_output.json - KUSAMA_DIGEST: ${{ github.workspace}}/kusama-srtool-json/kusama_srtool_output.json - POLKADOT_DIGEST: ${{ github.workspace}}/polkadot-srtool-json/polkadot_srtool_output.json - PRE_RELEASE: ${{ github.event.inputs.pre_release }} - run: | - find ${{env.GITHUB_WORKSPACE}} -type f -name "*_srtool_output.json" - ls -al $ROCOCO_DIGEST - ls -al $WESTEND_DIGEST - ls -al $KUSAMA_DIGEST - ls -al $POLKADOT_DIGEST - - cd polkadot/scripts/ci/changelog - - ./bin/changelog ${GITHUB_REF} - ls -al release-notes.md - ls -al context.json - - - name: Archive artifact context.json - uses: actions/upload-artifact@v3 - with: - name: release-notes-context - path: | - polkadot/scripts/ci/changelog/context.json - **/*_srtool_output.json - - - name: Create draft release - id: create-release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Polkadot ${{ github.ref }} - body_path: ./polkadot/scripts/ci/changelog/release-notes.md - draft: true - - publish-runtimes: - runs-on: ubuntu-latest - needs: ["publish-draft-release"] - env: - RUNTIME_DIR: runtime - strategy: - matrix: - runtime: ["polkadot", "kusama", "westend", "rococo"] - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - name: Download artifacts - uses: actions/download-artifact@v3 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.0.0 - - name: Get runtime version - id: get-runtime-ver - run: | - echo "require './scripts/ci/github/lib.rb'" > script.rb - echo "puts get_runtime(runtime: \"${{ matrix.runtime }}\", runtime_dir: \"$RUNTIME_DIR\")" >> script.rb - - echo "Current folder: $PWD" - ls "$RUNTIME_DIR/${{ matrix.runtime }}" - runtime_ver=$(ruby script.rb) - echo "Found version: >$runtime_ver<" - echo "runtime_ver=$runtime_ver" >> $GITHUB_OUTPUT - - - name: Upload compressed ${{ matrix.runtime }} wasm - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.publish-draft-release.outputs.asset_upload_url }} - asset_path: "${{ matrix.runtime }}-runtime/${{ matrix.runtime }}_runtime.compact.compressed.wasm" - asset_name: ${{ matrix.runtime }}_runtime-v${{ steps.get-runtime-ver.outputs.runtime_ver }}.compact.compressed.wasm - asset_content_type: application/wasm - - post_to_matrix: - runs-on: ubuntu-latest - needs: publish-draft-release - strategy: - matrix: - channel: - - name: "RelEng: Polkadot Release Coordination" - room: '!cqAmzdIcbOFwrdrubV:parity.io' - - steps: - - name: Send Matrix message to ${{ matrix.channel.name }} - uses: s3krit/matrix-message-action@70ad3fb812ee0e45ff8999d6af11cafad11a6ecf # v0.0.3 - with: - room_id: ${{ matrix.channel.room }} - access_token: ${{ secrets.RELEASENOTES_MATRIX_V2_ACCESS_TOKEN }} - server: m.parity.io - message: | - **New version of polkadot tagged**: ${{ github.ref }}<br/> - Draft release created: ${{ needs.publish-draft-release.outputs.release_url }} diff --git a/polkadot/.github/workflows/release-99_bot.yml b/polkadot/.github/workflows/release-99_bot.yml deleted file mode 100644 index 5d45c0d44eda3..0000000000000 --- a/polkadot/.github/workflows/release-99_bot.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Release - Send new release notification to matrix channels -on: - release: - types: - - published - -jobs: - ping_matrix: - strategy: - matrix: - channel: - - name: '#KusamaValidatorLounge:polkadot.builders' - room: '!LhjZccBOqFNYKLdmbb:polkadot.builders' - pre-releases: false - - name: '#kusama-announcements:matrix.parity.io' - room: '!FMwxpQnYhRCNDRsYGI:matrix.parity.io' - pre-release: false - - name: '#polkadotvalidatorlounge:web3.foundation' - room: '!NZrbtteFeqYKCUGQtr:matrix.parity.io' - pre-release: false - - name: '#polkadot-announcements:matrix.parity.io' - room: '!UqHPWiCBGZWxrmYBkF:matrix.parity.io' - pre-release: false - - name: "RelEng: Polkadot Release Coordination" - room: '!cqAmzdIcbOFwrdrubV:parity.io' - pre-release: true - - name: 'Ledger <> Polkadot Coordination' - room: '!EoIhaKfGPmFOBrNSHT:web3.foundation' - pre-release: true - - name: 'General: Rust, Polkadot, Substrate' - room: '!aJymqQYtCjjqImFLSb:parity.io' - pre-release: false - - name: 'Team: DevOps' - room: '!lUslSijLMgNcEKcAiE:parity.io' - pre-release: true - - runs-on: ubuntu-latest - steps: - - name: Send Matrix message to ${{ matrix.channel.name }} - if: github.event.release.prerelease == false || matrix.channel.pre-release - uses: s3krit/matrix-message-action@70ad3fb812ee0e45ff8999d6af11cafad11a6ecf # v0.0.3 - with: - room_id: ${{ matrix.channel.room }} - access_token: ${{ secrets.RELEASENOTES_MATRIX_V2_ACCESS_TOKEN }} - server: m.parity.io - message: | - ***Polkadot ${{github.event.release.tag_name}} has been released!***<br/> - ${{github.event.release.html_url}}<br/><br/> - ${{github.event.release.body}}<br/> diff --git a/polkadot/.gitlab-ci.yml b/polkadot/.gitlab-ci.yml deleted file mode 100644 index b2d91e61da94a..0000000000000 --- a/polkadot/.gitlab-ci.yml +++ /dev/null @@ -1,287 +0,0 @@ -# .gitlab-ci.yml -# -# polkadot -# -# Pipelines can be triggered manually in the web. -# -# Please do not add new jobs without "rules:" and "*-env". There are &test-refs for everything, -# "docker-env" is used for Rust jobs. -# And "kubernetes-env" for everything else. Please mention "image:" container name to be used -# with it, as there's no default one. - -# All jobs are sorted according to their duration using DAG mechanism -# Currently, test-linux-stable job is the longest one and other jobs are -# sorted in order to complete during this job and occupy less runners in one -# moment of time. - -stages: - - .pre - - weights - - check - - test - - build - - publish - - zombienet - - short-benchmarks - -workflow: - rules: - - if: $CI_COMMIT_TAG - - if: $CI_COMMIT_BRANCH - -variables: - GIT_STRATEGY: fetch - GIT_DEPTH: 100 - CI_SERVER_NAME: "GitLab CI" - CI_IMAGE: !reference [.ci-unified, variables, CI_IMAGE] - BUILDAH_IMAGE: "quay.io/buildah/stable:v1.29" - BUILDAH_COMMAND: "buildah --storage-driver overlay2" - DOCKER_OS: "debian:stretch" - ARCH: "x86_64" - ZOMBIENET_IMAGE: "docker.io/paritytech/zombienet:v1.3.65" - -default: - cache: {} - retry: - max: 2 - when: - - runner_system_failure - - unknown_failure - - api_failure - interruptible: true - -.common-before-script: - before_script: - - !reference [.job-switcher, before_script] - - !reference [.timestamp, before_script] - -.collect-artifacts: - artifacts: - name: "${CI_JOB_NAME}_${CI_COMMIT_REF_NAME}" - when: on_success - expire_in: 7 days - paths: - - ./artifacts/ - -.collect-artifacts-short: - artifacts: - name: "${CI_JOB_NAME}_${CI_COMMIT_REF_NAME}" - when: on_success - expire_in: 1 days - paths: - - ./artifacts/ - -# collecting vars for pipeline stopper -# they will be used if the job fails -.pipeline-stopper-vars: - before_script: - - echo "FAILED_JOB_URL=${CI_JOB_URL}" > pipeline-stopper.env - - echo "FAILED_JOB_NAME=${CI_JOB_NAME}" >> pipeline-stopper.env - - echo "FAILED_JOB_NAME=${CI_JOB_NAME}" >> pipeline-stopper.env - - echo "PR_NUM=${CI_COMMIT_REF_NAME}" >> pipeline-stopper.env - -.pipeline-stopper-artifacts: - artifacts: - reports: - dotenv: pipeline-stopper.env - -.job-switcher: - before_script: - - if echo "$CI_DISABLED_JOBS" | grep -xF "$CI_JOB_NAME"; then echo "The job has been cancelled in CI settings"; exit 0; fi - -.kubernetes-env: - image: "${CI_IMAGE}" - before_script: - - !reference [.common-before-script, before_script] - tags: - - kubernetes-parity-build - -.docker-env: - image: "${CI_IMAGE}" - before_script: - - !reference [.common-before-script, before_script] - tags: - - linux-docker-vm-c2 - -.compiler-info: - before_script: - - !reference [.common-before-script, before_script] - - rustup show - - cargo --version - -.test-refs: - rules: - - if: $CI_PIPELINE_SOURCE == "web" - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_COMMIT_REF_NAME == "master" - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - -.common-refs: - # these jobs run always* - rules: - - if: $CI_PIPELINE_SOURCE == "web" - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_COMMIT_REF_NAME == "master" - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - - if: $CI_COMMIT_REF_NAME =~ /^v[0-9]+\.[0-9]+.*$/ # i.e. v1.0, v2.1rc1 - - if: $CI_COMMIT_REF_NAME =~ /^release-v[0-9]+\.[0-9]+.*$/ # i.e. release-v0.9.27 - -.test-pr-refs: - rules: - - if: $CI_PIPELINE_SOURCE == "pipeline" - when: never - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - -.zombienet-refs: - rules: - - if: $CI_PIPELINE_SOURCE == "pipeline" - when: never - - if: $CI_PIPELINE_SOURCE == "schedule" - when: never - - if: $CI_COMMIT_REF_NAME == "master" - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - -.deploy-testnet-refs: - rules: - - if: $CI_PIPELINE_SOURCE == "pipeline" - when: never - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_COMMIT_REF_NAME == "master" - -.publish-refs: - rules: - - if: $CI_PIPELINE_SOURCE == "pipeline" - when: never - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_PIPELINE_SOURCE == "web" && - $CI_COMMIT_REF_NAME == "master" - - if: $CI_COMMIT_REF_NAME == "master" - - if: $CI_COMMIT_REF_NAME =~ /^v[0-9]+\.[0-9]+.*$/ # i.e. v1.0, v2.1rc1 - -.build-push-image: - variables: - CI_IMAGE: "${BUILDAH_IMAGE}" - - REGISTRY: "docker.io" - DOCKER_OWNER: "paritypr" - DOCKER_USER: "${PARITYPR_USER}" - DOCKER_PASS: "${PARITYPR_PASS}" - IMAGE: "${REGISTRY}/${DOCKER_OWNER}/${IMAGE_NAME}" - - ENGINE: "${BUILDAH_COMMAND}" - BUILDAH_FORMAT: "docker" - SKIP_IMAGE_VALIDATION: 1 - - PROJECT_ROOT: "." - BIN_FOLDER: "./artifacts" - VCS_REF: "${CI_COMMIT_SHA}" - - before_script: - - !reference [.common-before-script, before_script] - - test -s ./artifacts/VERSION || exit 1 - - test -s ./artifacts/EXTRATAG || exit 1 - - export VERSION="$(cat ./artifacts/VERSION)" - - EXTRATAG="$(cat ./artifacts/EXTRATAG)" - - echo "Polkadot version = ${VERSION} (EXTRATAG = ${EXTRATAG})" - script: - - test "$DOCKER_USER" -a "$DOCKER_PASS" || - ( echo "no docker credentials provided"; exit 1 ) - - TAGS="${VERSION},${EXTRATAG}" scripts/ci/dockerfiles/build-injected.sh - - echo "$DOCKER_PASS" | - buildah login --username "$DOCKER_USER" --password-stdin "${REGISTRY}" - - $BUILDAH_COMMAND info - - $BUILDAH_COMMAND push --format=v2s2 "$IMAGE:$VERSION" - - $BUILDAH_COMMAND push --format=v2s2 "$IMAGE:$EXTRATAG" - after_script: - - buildah logout --all - -#### stage: .pre - -# By default our pipelines are interruptible, but some special pipelines shouldn't be interrupted: -# * multi-project pipelines such as the ones triggered by the scripts repo -# -# In those cases, we add an uninterruptible .pre job; once that one has started, -# the entire pipeline becomes uninterruptible. -uninterruptible-pipeline: - extends: .kubernetes-env - variables: - CI_IMAGE: "paritytech/tools:latest" - stage: .pre - interruptible: false - rules: - - if: $CI_PIPELINE_SOURCE == "pipeline" - script: "true" - -include: - # weights jobs - - scripts/ci/gitlab/pipeline/weights.yml - # check jobs - - scripts/ci/gitlab/pipeline/check.yml - # test jobs - - scripts/ci/gitlab/pipeline/test.yml - # build jobs - - scripts/ci/gitlab/pipeline/build.yml - # short-benchmarks jobs - - scripts/ci/gitlab/pipeline/short-benchmarks.yml - # publish jobs - - scripts/ci/gitlab/pipeline/publish.yml - # zombienet jobs - - scripts/ci/gitlab/pipeline/zombienet.yml - # timestamp handler - - project: parity/infrastructure/ci_cd/shared - ref: main - file: /common/timestamp.yml - - project: parity/infrastructure/ci_cd/shared - ref: main - file: /common/ci-unified.yml - - -#### stage: .post - -deploy-parity-testnet: - stage: .post - extends: - - .deploy-testnet-refs - variables: - POLKADOT_CI_COMMIT_NAME: "${CI_COMMIT_REF_NAME}" - POLKADOT_CI_COMMIT_REF: "${CI_COMMIT_SHORT_SHA}" - allow_failure: false - trigger: "parity/infrastructure/parity-testnet" - -# This job cancels the whole pipeline if any of provided jobs fail. -# In a DAG, every jobs chain is executed independently of others. The `fail_fast` principle suggests -# to fail the pipeline as soon as possible to shorten the feedback loop. -.cancel-pipeline-template: - stage: .post - rules: - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - when: on_failure - variables: - PROJECT_ID: "${CI_PROJECT_ID}" - PROJECT_NAME: "${CI_PROJECT_NAME}" - PIPELINE_ID: "${CI_PIPELINE_ID}" - FAILED_JOB_URL: "${FAILED_JOB_URL}" - FAILED_JOB_NAME: "${FAILED_JOB_NAME}" - PR_NUM: "${PR_NUM}" - trigger: - project: "parity/infrastructure/ci_cd/pipeline-stopper" - branch: "as-improve" - -remove-cancel-pipeline-message: - stage: .post - rules: - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - variables: - PROJECT_ID: "${CI_PROJECT_ID}" - PROJECT_NAME: "${CI_PROJECT_NAME}" - PIPELINE_ID: "${CI_PIPELINE_ID}" - FAILED_JOB_URL: "https://gitlab.com" - FAILED_JOB_NAME: "nope" - PR_NUM: "${CI_COMMIT_REF_NAME}" - trigger: - project: "parity/infrastructure/ci_cd/pipeline-stopper" - -cancel-pipeline-test-linux-stable: - extends: .cancel-pipeline-template - needs: - - job: test-linux-stable diff --git a/substrate/.gitattributes b/substrate/.gitattributes index a77c52fccdb77..4cb3ef4972feb 100644 --- a/substrate/.gitattributes +++ b/substrate/.gitattributes @@ -1,4 +1,2 @@ Cargo.lock linguist-generated=true -/.gitlab-ci.yml filter=ci-prettier -/scripts/ci/gitlab/pipeline/*.yml filter=ci-prettier frame/**/src/weights.rs linguist-generated=true diff --git a/substrate/.github/dependabot.yml b/substrate/.github/dependabot.yml deleted file mode 100644 index 04cf0d1e1a5a4..0000000000000 --- a/substrate/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "cargo" - directory: "/" - labels: ["A2-insubstantial", "B0-silent", "C1-low", "E2-dependencies"] - schedule: - interval: "daily" - - package-ecosystem: github-actions - directory: '/' - labels: ["A2-insubstantial", "B0-silent", "C1-low", "E2-dependencies"] - schedule: - interval: daily diff --git a/substrate/.github/pr-custom-review.yml b/substrate/.github/pr-custom-review.yml deleted file mode 100644 index 059f4a283af07..0000000000000 --- a/substrate/.github/pr-custom-review.yml +++ /dev/null @@ -1,39 +0,0 @@ -# 🔒 PROTECTED: Changes to locks-review-team should be approved by the current locks-review-team -locks-review-team: locks-review -team-leads-team: polkadot-review -action-review-team: ci - -rules: - - name: Core developers - check_type: changed_files - condition: - include: .* - # excluding files from 'CI team' and 'FRAME coders' rules - exclude: ^\.gitlab-ci\.yml|^scripts/ci/.*|^\.github/.*|^\.config/nextest.toml|^frame/(?!.*(nfts/.*|uniques/.*|babe/.*|grandpa/.*|beefy|merkle-mountain-range/.*|contracts/.*|election|nomination-pools/.*|staking/.*|aura/.*)) - min_approvals: 2 - teams: - - core-devs - - - name: FRAME coders - check_type: changed_files - condition: - include: ^frame/(?!.*(nfts/.*|uniques/.*|babe/.*|grandpa/.*|beefy|merkle-mountain-range/.*|contracts/.*|election|nomination-pools/.*|staking/.*|aura/.*)) - all: - - min_approvals: 2 - teams: - - core-devs - - min_approvals: 1 - teams: - - frame-coders - - - name: CI team - check_type: changed_files - condition: - include: ^\.gitlab-ci\.yml|^scripts/ci/.*|^\.github/.*|^\.config/nextest.toml - min_approvals: 2 - teams: - - ci - -prevent-review-request: - teams: - - core-devs diff --git a/substrate/.github/stale.yml b/substrate/.github/stale.yml deleted file mode 100644 index 61d0fd0228d97..0000000000000 --- a/substrate/.github/stale.yml +++ /dev/null @@ -1,18 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 30 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 14 -# Issues with these labels will never be considered stale -exemptLabels: - - "D9-needsaudit 👮" -# Label to use when marking an issue as stale -staleLabel: "A3-stale" -# we only bother with pull requests -only: pulls -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - Hey, is anyone still working on this? Due to the inactivity this issue has - been automatically marked as stale. It will be closed if no further activity - occurs. Thank you for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/substrate/.github/workflows/auto-label-issues.yml b/substrate/.github/workflows/auto-label-issues.yml deleted file mode 100644 index 12ffce702cdcc..0000000000000 --- a/substrate/.github/workflows/auto-label-issues.yml +++ /dev/null @@ -1,17 +0,0 @@ -# If the author of the issues is not a contributor to the project, label -# the issue with 'Z0-unconfirmed' - -name: Label New Issues -on: - issues: - types: [opened] - -jobs: - label-new-issues: - runs-on: ubuntu-latest - steps: - - name: Label drafts - uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90 # 1.0.4 - if: github.event.issue.author_association == 'NONE' - with: - add-labels: "I10-unconfirmed" diff --git a/substrate/.github/workflows/burnin-label-notification.yml b/substrate/.github/workflows/burnin-label-notification.yml deleted file mode 100644 index f45455d31db1e..0000000000000 --- a/substrate/.github/workflows/burnin-label-notification.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Notify devops when burn-in label applied -on: - pull_request: - types: [labeled] - -jobs: - notify-devops: - runs-on: ubuntu-latest - strategy: - matrix: - channel: - - name: 'Team: DevOps' - room: '!lUslSijLMgNcEKcAiE:parity.io' - - steps: - - name: Notify devops - if: startsWith(github.event.label.name, 'A1-') - uses: s3krit/matrix-message-action@70ad3fb812ee0e45ff8999d6af11cafad11a6ecf # v0.0.3 - with: - room_id: ${{ matrix.channel.room }} - access_token: ${{ secrets.RELEASENOTES_MATRIX_V2_ACCESS_TOKEN }} - server: "m.parity.io" - message: | - @room Burn-in request received for [${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url }}) diff --git a/substrate/.github/workflows/check-D-labels.yml b/substrate/.github/workflows/check-D-labels.yml deleted file mode 100644 index 7bb358ce1182e..0000000000000 --- a/substrate/.github/workflows/check-D-labels.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Check D labels - -on: - pull_request: - types: [labeled, opened, synchronize, unlabeled] - paths: - - frame/** - - primitives/** - -env: - IMAGE: paritytech/ruled_labels:0.4.0 - -jobs: - check-labels: - runs-on: ubuntu-latest - steps: - - name: Pull image - run: docker pull $IMAGE - - - name: Check labels - env: - MOUNT: /work - GITHUB_PR: ${{ github.event.pull_request.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - API_BASE: https://api.github.com/repos - REPO: ${{ github.repository }} - RULES_PATH: labels/ruled_labels - CHECK_SPECS: specs_substrate.yaml - run: | - echo "REPO: ${REPO}" - echo "GITHUB_PR: ${GITHUB_PR}" - # Clone repo with labels specs - git clone https://github.com/paritytech/labels - # Fetch the labels for the PR under test - labels=$( curl -H "Authorization: token ${GITHUB_TOKEN}" -s "$API_BASE/${REPO}/pulls/${GITHUB_PR}" | jq '.labels | .[] | .name' | tr "\n" ",") - - if [ -z "${labels}" ]; then - docker run --rm -i -v $PWD/${RULES_PATH}/:$MOUNT $IMAGE check $MOUNT/$CHECK_SPECS --tags audit --no-label - fi - - labels_args=${labels: :-1} - printf "Checking labels: %s\n" "${labels_args}" - - # Prevent the shell from splitting labels with spaces - IFS="," - - # --dev is more useful to debug mode to debug - docker run --rm -i -v $PWD/${RULES_PATH}/:$MOUNT $IMAGE check $MOUNT/$CHECK_SPECS --labels ${labels_args} --dev --tags audit diff --git a/substrate/.github/workflows/check-labels.yml b/substrate/.github/workflows/check-labels.yml deleted file mode 100644 index 55b8f7389fa7f..0000000000000 --- a/substrate/.github/workflows/check-labels.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Check labels - -on: - pull_request: - types: [labeled, opened, synchronize, unlabeled] - -env: - IMAGE: paritytech/ruled_labels:0.4.0 - -jobs: - check-labels: - runs-on: ubuntu-latest - steps: - - name: Pull image - run: docker pull $IMAGE - - - name: Check labels - env: - MOUNT: /work - GITHUB_PR: ${{ github.event.pull_request.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - API_BASE: https://api.github.com/repos - REPO: ${{ github.repository }} - RULES_PATH: labels/ruled_labels - CHECK_SPECS: specs_substrate.yaml - run: | - echo "REPO: ${REPO}" - echo "GITHUB_PR: ${GITHUB_PR}" - # Clone repo with labels specs - git clone https://github.com/paritytech/labels - # Fetch the labels for the PR under test - labels=$( curl -H "Authorization: token ${GITHUB_TOKEN}" -s "$API_BASE/${REPO}/pulls/${GITHUB_PR}" | jq '.labels | .[] | .name' | tr "\n" ",") - - if [ -z "${labels}" ]; then - docker run --rm -i -v $PWD/${RULES_PATH}/:$MOUNT $IMAGE check $MOUNT/$CHECK_SPECS --tags PR --no-label - fi - - labels_args=${labels: :-1} - printf "Checking labels: %s\n" "${labels_args}" - - # Prevent the shell from splitting labels with spaces - IFS="," - - # --dev is more useful to debug mode to debug - docker run --rm -i -v $PWD/${RULES_PATH}/:$MOUNT $IMAGE check $MOUNT/$CHECK_SPECS --labels ${labels_args} --dev --tags PR diff --git a/substrate/.github/workflows/md-link-check.yml b/substrate/.github/workflows/md-link-check.yml deleted file mode 100644 index e1387f6da13f7..0000000000000 --- a/substrate/.github/workflows/md-link-check.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Check Links - -on: - pull_request: - branches: - - master - push: - branches: - - master - -jobs: - markdown-link-check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: gaurav-nelson/github-action-markdown-link-check@0a51127e9955b855a9bbfa1ff5577f1d1338c9a5 # 1.0.14 - with: - use-quiet-mode: 'yes' - config-file: '.github/workflows/mlc_config.json' diff --git a/substrate/.github/workflows/mlc_config.json b/substrate/.github/workflows/mlc_config.json deleted file mode 100644 index e7e620b39e0a9..0000000000000 --- a/substrate/.github/workflows/mlc_config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ignorePatterns": [ - { - "pattern": "^https://crates.io", - } - ] -} diff --git a/substrate/.github/workflows/monthly-tag.yml b/substrate/.github/workflows/monthly-tag.yml deleted file mode 100644 index 055207d85a4dd..0000000000000 --- a/substrate/.github/workflows/monthly-tag.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Monthly Snapshot Tag - -on: - schedule: - - cron: "0 1 1 * *" - workflow_dispatch: - -jobs: - build: - name: Take Snapshot - runs-on: ubuntu-latest - steps: - - name: Get the tags by date - id: tags - run: | - echo "new=$(date +'monthly-%Y-%m')" >> $GITHUB_OUTPUT - echo "old=$(date -d'1 month ago' +'monthly-%Y-%m')" >> $GITHUB_OUTPUT - - name: Checkout branch "master" - uses: actions/checkout@v3 - with: - ref: 'master' - fetch-depth: 0 - - name: Generate changelog - id: changelog - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - echo "# Automatic snapshot pre-release ${{ steps.tags.outputs.new }}" > Changelog.md - echo "" >> Changelog.md - echo "## Changes since last snapshot (${{ steps.tags.outputs.old }})" >> Changelog.md - echo "" >> Changelog.md - ./scripts/ci/github/generate_changelog.sh ${{ steps.tags.outputs.old }} >> Changelog.md - - name: Release snapshot - id: release-snapshot - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4 latest version, repo archived - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.tags.outputs.new }} - release_name: ${{ steps.tags.outputs.new }} - draft: false - prerelease: true - body_path: Changelog.md diff --git a/substrate/.github/workflows/pr-custom-review.yml b/substrate/.github/workflows/pr-custom-review.yml deleted file mode 100644 index 8e40c9ee72989..0000000000000 --- a/substrate/.github/workflows/pr-custom-review.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Assign reviewers - -on: - pull_request: - branches: - - master - - main - types: - - opened - - reopened - - synchronize - - review_requested - - review_request_removed - - ready_for_review - - converted_to_draft - pull_request_review: - -jobs: - pr-custom-review: - runs-on: ubuntu-latest - steps: - - name: Skip if pull request is in Draft - # `if: github.event.pull_request.draft == true` should be kept here, at - # the step level, rather than at the job level. The latter is not - # recommended because when the PR is moved from "Draft" to "Ready to - # review" the workflow will immediately be passing (since it was skipped), - # even though it hasn't actually ran, since it takes a few seconds for - # the workflow to start. This is also disclosed in: - # https://github.community/t/dont-run-actions-on-draft-pull-requests/16817/17 - # That scenario would open an opportunity for the check to be bypassed: - # 1. Get your PR approved - # 2. Move it to Draft - # 3. Push whatever commits you want - # 4. Move it to "Ready for review"; now the workflow is passing (it was - # skipped) and "Check reviews" is also passing (it won't be updated - # until the workflow is finished) - if: github.event.pull_request.draft == true - run: exit 1 - - name: pr-custom-review - uses: paritytech/pr-custom-review@action-v3 - with: - checks-reviews-api: http://pcr.parity-prod.parity.io/api/v1/check_reviews diff --git a/substrate/.github/workflows/release-bot.yml b/substrate/.github/workflows/release-bot.yml deleted file mode 100644 index 05bea32abc697..0000000000000 --- a/substrate/.github/workflows/release-bot.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Pushes release updates to a pre-defined Matrix room -on: - release: - types: - - edited - - prereleased - - published -jobs: - ping_matrix: - runs-on: ubuntu-latest - strategy: - matrix: - channel: - - name: 'General: Rust, Polkadot, Substrate' - room: '!aJymqQYtCjjqImFLSb:parity.io' - pre-release: false - - steps: - - name: send message - uses: s3krit/matrix-message-action@70ad3fb812ee0e45ff8999d6af11cafad11a6ecf # v0.0.3 - with: - room_id: ${{ matrix.channel.room }} - access_token: ${{ secrets.RELEASENOTES_MATRIX_V2_ACCESS_TOKEN }} - server: "m.parity.io" - message: | - ***${{github.event.repository.full_name}}:*** A release has been ${{github.event.action}}<br/> - Release version [${{github.event.release.tag_name}}](${{github.event.release.html_url}}) - - ----- - - ${{github.event.release.body}}<br/> diff --git a/substrate/.github/workflows/release-tagging.yml b/substrate/.github/workflows/release-tagging.yml deleted file mode 100644 index 1862582f40eba..0000000000000 --- a/substrate/.github/workflows/release-tagging.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Github action to ensure the `release` tag always tracks latest release - -name: Retag release - -on: - release: - types: [ published ] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Set Git tag - uses: s3krit/walking-tag-action@d04f7a53b72ceda4e20283736ce3627011275178 # latest version from master - with: - tag-name: release - tag-message: Latest release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/substrate/.gitlab-ci.yml b/substrate/.gitlab-ci.yml deleted file mode 100644 index f00836528973e..0000000000000 --- a/substrate/.gitlab-ci.yml +++ /dev/null @@ -1,412 +0,0 @@ -# .gitlab-ci.yml -# -# substrate -# -# pipelines can be triggered manually in the web -# -# Currently the file is divided into subfiles. Each stage has a different file which -# can be found here: scripts/ci/gitlab/pipeline/<stage_name>.yml -# -# Instead of YAML anchors "extends" is used. -# Useful links: -# https://docs.gitlab.com/ee/ci/yaml/index.html#extends -# https://docs.gitlab.com/ee/ci/yaml/yaml_optimization.html#reference-tags -# -# SAMPLE JOB TEMPLATE - This is not a complete example but is enough to build a -# simple CI job. For full documentation, visit https://docs.gitlab.com/ee/ci/yaml/ -# -# my-example-job: -# stage: test # One of the stages listed below this job (required) -# image: paritytech/tools:latest # Any docker image (required) -# allow_failure: true # Allow the pipeline to continue if this job fails (default: false) -# needs: -# - job: test-linux # Any jobs that are required to run before this job (optional) -# variables: -# MY_ENVIRONMENT_VARIABLE: "some useful value" # Environment variables passed to the job (optional) -# script: -# - echo "List of shell commands to run in your job" -# - echo "You can also just specify a script here, like so:" -# - ./scripts/ci/gitlab/my_amazing_script.sh - -stages: - - check - - test - - build - - publish - - notify - - zombienet - - deploy - -workflow: - rules: - - if: $CI_COMMIT_TAG - - if: $CI_COMMIT_BRANCH - -variables: - GIT_STRATEGY: fetch - GIT_DEPTH: 100 - CARGO_INCREMENTAL: 0 - DOCKER_OS: "debian:bullseye" - ARCH: "x86_64" - CI_IMAGE: !reference [.ci-unified, variables, CI_IMAGE] - BUILDAH_IMAGE: "quay.io/buildah/stable:v1.29" - BUILDAH_COMMAND: "buildah --storage-driver overlay2" - RELENG_SCRIPTS_BRANCH: "master" - - RUSTY_CACHIER_SINGLE_BRANCH: master - RUSTY_CACHIER_DONT_OPERATE_ON_MAIN_BRANCH: "true" - RUSTY_CACHIER_MINIO_ALIAS: rustycachier_gcs - RUSTY_CACHIER_MINIO_BUCKET: parity-build-rusty-cachier - RUSTY_CACHIER_COMPRESSION_METHOD: zstd - - NEXTEST_FAILURE_OUTPUT: immediate-final - NEXTEST_SUCCESS_OUTPUT: final - ZOMBIENET_IMAGE: "docker.io/paritytech/zombienet:v1.3.55" - -default: - retry: - max: 2 - when: - - runner_system_failure - - unknown_failure - - api_failure - cache: {} - interruptible: true - -.collect-artifacts: - artifacts: - name: "${CI_JOB_NAME}_${CI_COMMIT_REF_NAME}" - when: on_success - expire_in: 7 days - paths: - - artifacts/ - -.collect-artifacts-short: - artifacts: - name: "${CI_JOB_NAME}_${CI_COMMIT_REF_NAME}" - when: on_success - expire_in: 3 hours - paths: - - artifacts/ - -.prepare-env: - before_script: - # TODO: remove unset invocation when we'll be free from 'ENV RUSTC_WRAPPER=sccache' & sccache - # itself in all images - - unset RUSTC_WRAPPER - # $WASM_BUILD_WORKSPACE_HINT enables wasm-builder to find the Cargo.lock from within generated - # packages - - export WASM_BUILD_WORKSPACE_HINT="$PWD" - # ensure that RUSTFLAGS are set correctly - - echo $RUSTFLAGS - -.job-switcher: - before_script: - - if echo "$CI_DISABLED_JOBS" | grep -xF "$CI_JOB_NAME"; then echo "The job has been cancelled in CI settings"; exit 0; fi - -.kubernetes-env: - image: "${CI_IMAGE}" - before_script: - - !reference [.timestamp, before_script] - - !reference [.job-switcher, before_script] - - !reference [.prepare-env, before_script] - tags: - - kubernetes-parity-build - -.rust-info-script: - script: - - rustup show - - cargo --version - - rustup +nightly show - - cargo +nightly --version - -.pipeline-stopper-vars: - script: - - !reference [.job-switcher, before_script] - - echo "Collecting env variables for the cancel-pipeline job" - - echo "FAILED_JOB_URL=${CI_JOB_URL}" > pipeline-stopper.env - - echo "FAILED_JOB_NAME=${CI_JOB_NAME}" >> pipeline-stopper.env - - echo "PR_NUM=${CI_COMMIT_REF_NAME}" >> pipeline-stopper.env - -.pipeline-stopper-artifacts: - artifacts: - reports: - dotenv: pipeline-stopper.env - -.docker-env: - image: "${CI_IMAGE}" - before_script: - - !reference [.timestamp, before_script] - - !reference [.job-switcher, before_script] - - !reference [.prepare-env, before_script] - - !reference [.rust-info-script, script] - - !reference [.rusty-cachier, before_script] - - !reference [.pipeline-stopper-vars, script] - after_script: - - !reference [.rusty-cachier, after_script] - tags: - - linux-docker-vm-c2 - -# rusty-cachier's hidden job. Parts of this job are used to instrument the pipeline's other real jobs with rusty-cachier -# Description of the commands is available here - https://gitlab.parity.io/parity/infrastructure/ci_cd/rusty-cachier/client#description -.rusty-cachier: - before_script: - - curl -s https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.parity.io/parity/infrastructure/ci_cd/rusty-cachier/client/-/raw/release/util/install.sh | bash - - rusty-cachier environment check --gracefully - - $(rusty-cachier environment inject) - - rusty-cachier project mtime - after_script: - - env RUSTY_CACHIER_SUPRESS_OUTPUT=true rusty-cachier snapshot destroy - -.test-refs: - rules: - - if: $CI_PIPELINE_SOURCE == "web" - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_COMMIT_REF_NAME == "master" - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - - if: $CI_COMMIT_REF_NAME =~ /^v[0-9]+\.[0-9]+.*$/ # i.e. v1.0, v2.1rc1 - -# handle the specific case where benches could store incorrect bench data because of the downstream staging runs -# exclude cargo-check-benches from such runs -.test-refs-check-benches: - rules: - - if: $CI_COMMIT_REF_NAME == "master" && $CI_PIPELINE_SOURCE == "pipeline" && $CI_IMAGE =~ /staging$/ - when: never - - if: $CI_PIPELINE_SOURCE == "web" - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_COMMIT_REF_NAME == "master" - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - - if: $CI_COMMIT_REF_NAME =~ /^v[0-9]+\.[0-9]+.*$/ # i.e. v1.0, v2.1rc1 - -.test-refs-no-trigger: - rules: - - if: $CI_PIPELINE_SOURCE == "pipeline" - when: never - - if: $CI_PIPELINE_SOURCE == "web" - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_COMMIT_REF_NAME == "master" - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - - if: $CI_COMMIT_REF_NAME =~ /^v[0-9]+\.[0-9]+.*$/ # i.e. v1.0, v2.1rc1 - - if: $CI_COMMIT_REF_NAME =~ /^ci-release-.*$/ - -.test-refs-no-trigger-prs-only: - rules: - - if: $CI_PIPELINE_SOURCE == "pipeline" - when: never - - if: $CI_PIPELINE_SOURCE == "web" - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - -.publish-refs: - rules: - - if: $CI_PIPELINE_SOURCE == "pipeline" - when: never - - if: $CI_PIPELINE_SOURCE == "web" - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_COMMIT_REF_NAME == "master" - - if: $CI_COMMIT_REF_NAME =~ /^v[0-9]+\.[0-9]+.*$/ # i.e. v1.0, v2.1rc1 - -.build-refs: - # publish-refs + PRs - rules: - - if: $CI_PIPELINE_SOURCE == "pipeline" - when: never - - if: $CI_PIPELINE_SOURCE == "web" - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_COMMIT_REF_NAME == "master" - - if: $CI_COMMIT_REF_NAME =~ /^v[0-9]+\.[0-9]+.*$/ # i.e. v1.0, v2.1rc1 - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - -.zombienet-refs: - extends: .build-refs - -.crates-publishing-variables: - variables: - CRATESIO_CRATES_OWNER: parity-crate-owner - REPO: substrate - REPO_OWNER: paritytech - -.crates-publishing-pipeline: - extends: .crates-publishing-variables - rules: - - if: $CI_PIPELINE_SOURCE == "schedule" && $CI_COMMIT_REF_NAME == "master" && $PIPELINE == "automatic-crate-publishing" - -.crates-publishing-template: - extends: - - .docker-env - - .crates-publishing-variables - # collect artifacts even on failure so that we know how the crates were generated (they'll be - # generated to the artifacts folder according to SPUB_TMP further down) - artifacts: - name: "${CI_JOB_NAME}_${CI_COMMIT_REF_NAME}" - when: always - expire_in: 7 days - paths: - - artifacts/ - variables: - SPUB_TMP: artifacts - # disable timestamping for the crate publishing jobs, they leave stray child processes behind - # which don't interact well with the timestamping script - CI_DISABLE_TIMESTAMP: 1 - -#### stage: .pre - -check-crates-publishing-pipeline: - stage: .pre - extends: - - .kubernetes-env - - .crates-publishing-pipeline - script: - - git clone - --depth 1 - --branch "$RELENG_SCRIPTS_BRANCH" - https://github.com/paritytech/releng-scripts.git - - ONLY_CHECK_PIPELINE=true ./releng-scripts/publish-crates - -# By default our pipelines are interruptible, but some special pipelines shouldn't be interrupted: -# * multi-project pipelines such as the ones triggered by the scripts repo -# * the scheduled automatic-crate-publishing pipeline -# -# In those cases, we add an uninterruptible .pre job; once that one has started, -# the entire pipeline becomes uninterruptible -uninterruptible-pipeline: - extends: .kubernetes-env - variables: - CI_IMAGE: "paritytech/tools:latest" - stage: .pre - interruptible: false - rules: - - if: $CI_PIPELINE_SOURCE == "pipeline" - - if: $CI_PIPELINE_SOURCE == "schedule" && $PIPELINE == "automatic-crate-publishing" - script: "true" - -include: - # check jobs - - scripts/ci/gitlab/pipeline/check.yml - # tests jobs - - scripts/ci/gitlab/pipeline/test.yml - # build jobs - - scripts/ci/gitlab/pipeline/build.yml - # publish jobs - - scripts/ci/gitlab/pipeline/publish.yml - # zombienet jobs - - scripts/ci/gitlab/pipeline/zombienet.yml - # The crate-publishing pipeline requires a customized `interruptible` configuration. Unfortunately - # `interruptible` can't currently be dynamically set based on variables as per: - # - https://gitlab.com/gitlab-org/gitlab/-/issues/38349 - # - https://gitlab.com/gitlab-org/gitlab/-/issues/194023 - # Thus we work around that limitation by using conditional includes. - # For crate-publishing pipelines: run it with defaults + `interruptible: false`. The WHOLE - # pipeline is made uninterruptible to ensure that test jobs also get a chance to run to - # completion, because the publishing jobs depends on them AS INTENDED: crates should not be - # published before their source code is checked. - - project: parity/infrastructure/ci_cd/shared - ref: main - file: /common/timestamp.yml - - project: parity/infrastructure/ci_cd/shared - ref: main - file: /common/ci-unified.yml - - -#### stage: notify - -# This job notifies rusty-cachier about the latest commit with the cache. -# This info is later used for the cache distribution and an overlay creation. -# Note that we don't use any .rusty-cachier references as we assume that a pipeline has reached this stage with working rusty-cachier. -rusty-cachier-notify: - stage: notify - extends: .kubernetes-env - variables: - CI_IMAGE: paritytech/rusty-cachier-env:latest - GIT_STRATEGY: none - dependencies: [] - script: - - curl -s https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.parity.io/parity/infrastructure/ci_cd/rusty-cachier/client/-/raw/release/util/install.sh | bash - - rusty-cachier cache notify - -#### stage: .post - -# This job cancels the whole pipeline if any of provided jobs fail. -# In a DAG, every jobs chain is executed independently of others. The `fail_fast` principle suggests -# to fail the pipeline as soon as possible to shorten the feedback loop. -.cancel-pipeline-template: - stage: .post - rules: - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - when: on_failure - variables: - PROJECT_ID: "${CI_PROJECT_ID}" - PROJECT_NAME: "${CI_PROJECT_NAME}" - PIPELINE_ID: "${CI_PIPELINE_ID}" - FAILED_JOB_URL: "${FAILED_JOB_URL}" - FAILED_JOB_NAME: "${FAILED_JOB_NAME}" - PR_NUM: "${PR_NUM}" - trigger: - project: "parity/infrastructure/ci_cd/pipeline-stopper" - -remove-cancel-pipeline-message: - stage: .post - rules: - - if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs - variables: - PROJECT_ID: "${CI_PROJECT_ID}" - PROJECT_NAME: "${CI_PROJECT_NAME}" - PIPELINE_ID: "${CI_PIPELINE_ID}" - FAILED_JOB_URL: "https://gitlab.com" - FAILED_JOB_NAME: "nope" - PR_NUM: "${CI_COMMIT_REF_NAME}" - trigger: - project: "parity/infrastructure/ci_cd/pipeline-stopper" - branch: "as-improve" - -# need to copy jobs this way because otherwise gitlab will wait -# for all 3 jobs to finish instead of cancelling if one fails -cancel-pipeline-test-linux-stable1: - extends: .cancel-pipeline-template - needs: - - job: "test-linux-stable 1/3" - -cancel-pipeline-test-linux-stable2: - extends: .cancel-pipeline-template - needs: - - job: "test-linux-stable 2/3" - -cancel-pipeline-test-linux-stable3: - extends: .cancel-pipeline-template - needs: - - job: "test-linux-stable 3/3" - -cancel-pipeline-cargo-check-benches1: - extends: .cancel-pipeline-template - needs: - - job: "cargo-check-benches 1/2" - -cancel-pipeline-cargo-check-benches2: - extends: .cancel-pipeline-template - needs: - - job: "cargo-check-benches 2/2" - -cancel-pipeline-test-linux-stable-int: - extends: .cancel-pipeline-template - needs: - - job: test-linux-stable-int - -cancel-pipeline-cargo-check-each-crate-1: - extends: .cancel-pipeline-template - needs: - - job: "cargo-check-each-crate 1/2" - -cancel-pipeline-cargo-check-each-crate-2: - extends: .cancel-pipeline-template - needs: - - job: "cargo-check-each-crate 2/2" - -cancel-pipeline-cargo-check-each-crate-macos: - extends: .cancel-pipeline-template - needs: - - job: cargo-check-each-crate-macos - -cancel-pipeline-check-tracing: - extends: .cancel-pipeline-template - needs: - - job: check-tracing From 51c0c24213aeed7b7c230835a7e381cc2147df82 Mon Sep 17 00:00:00 2001 From: Marcin S <marcin@realemail.net> Date: Thu, 5 Oct 2023 18:37:54 +0200 Subject: [PATCH 08/54] PVF: Add back socket path parameter, use tmp socket path (#1780) --- .../node/core/pvf/common/src/worker/mod.rs | 24 ++- .../node/core/pvf/common/src/worker_dir.rs | 5 - .../node/core/pvf/execute-worker/src/lib.rs | 4 + .../node/core/pvf/prepare-worker/src/lib.rs | 4 + polkadot/node/core/pvf/src/worker_intf.rs | 140 +++++++++++------- 5 files changed, 111 insertions(+), 66 deletions(-) diff --git a/polkadot/node/core/pvf/common/src/worker/mod.rs b/polkadot/node/core/pvf/common/src/worker/mod.rs index 59973f6cbbc64..d0bd5b6bd7c56 100644 --- a/polkadot/node/core/pvf/common/src/worker/mod.rs +++ b/polkadot/node/core/pvf/common/src/worker/mod.rs @@ -18,7 +18,7 @@ pub mod security; -use crate::{worker_dir, SecurityStatus, LOG_TARGET}; +use crate::{SecurityStatus, LOG_TARGET}; use cpu_time::ProcessTime; use futures::never::Never; use std::{ @@ -115,6 +115,7 @@ macro_rules! decl_worker_main { }, } + let mut socket_path = None; let mut worker_dir_path = None; let mut node_version = None; let mut can_enable_landlock = false; @@ -123,6 +124,10 @@ macro_rules! decl_worker_main { let mut i = 2; while i < args.len() { match args[i].as_ref() { + "--socket-path" => { + socket_path = Some(args[i + 1].as_str()); + i += 1 + }, "--worker-dir-path" => { worker_dir_path = Some(args[i + 1].as_str()); i += 1 @@ -138,16 +143,24 @@ macro_rules! decl_worker_main { } i += 1; } + let socket_path = socket_path.expect("the --socket-path argument is required"); let worker_dir_path = worker_dir_path.expect("the --worker-dir-path argument is required"); + let socket_path = std::path::Path::new(socket_path).to_owned(); let worker_dir_path = std::path::Path::new(worker_dir_path).to_owned(); let security_status = $crate::SecurityStatus { can_enable_landlock, can_unshare_user_namespace_and_change_root, }; - $entrypoint(worker_dir_path, node_version, Some($worker_version), security_status); + $entrypoint( + socket_path, + worker_dir_path, + node_version, + Some($worker_version), + security_status, + ); } }; } @@ -177,6 +190,7 @@ impl fmt::Display for WorkerKind { // the version that this crate was compiled with. pub fn worker_event_loop<F, Fut>( worker_kind: WorkerKind, + socket_path: PathBuf, #[cfg_attr(not(target_os = "linux"), allow(unused_mut))] mut worker_dir_path: PathBuf, node_version: Option<&str>, worker_version: Option<&str>, @@ -190,6 +204,7 @@ pub fn worker_event_loop<F, Fut>( gum::debug!( target: LOG_TARGET, %worker_pid, + ?socket_path, ?worker_dir_path, ?security_status, "starting pvf worker ({})", @@ -237,12 +252,9 @@ pub fn worker_event_loop<F, Fut>( } // Connect to the socket. - let socket_path = worker_dir::socket(&worker_dir_path); let stream = || -> std::io::Result<UnixStream> { let stream = UnixStream::connect(&socket_path)?; - // Remove the socket here. We don't also need to do this on the host-side; on failed - // rendezvous, the host will delete the whole worker dir. - std::fs::remove_file(&socket_path)?; + let _ = std::fs::remove_file(&socket_path); Ok(stream) }(); let stream = match stream { diff --git a/polkadot/node/core/pvf/common/src/worker_dir.rs b/polkadot/node/core/pvf/common/src/worker_dir.rs index c2610a4d11285..1cdf43a61e48b 100644 --- a/polkadot/node/core/pvf/common/src/worker_dir.rs +++ b/polkadot/node/core/pvf/common/src/worker_dir.rs @@ -20,7 +20,6 @@ use std::path::{Path, PathBuf}; const WORKER_EXECUTE_ARTIFACT_NAME: &str = "artifact"; const WORKER_PREPARE_TMP_ARTIFACT_NAME: &str = "tmp-artifact"; -const WORKER_SOCKET_NAME: &str = "socket"; pub fn execute_artifact(worker_dir_path: &Path) -> PathBuf { worker_dir_path.join(WORKER_EXECUTE_ARTIFACT_NAME) @@ -29,7 +28,3 @@ pub fn execute_artifact(worker_dir_path: &Path) -> PathBuf { pub fn prepare_tmp_artifact(worker_dir_path: &Path) -> PathBuf { worker_dir_path.join(WORKER_PREPARE_TMP_ARTIFACT_NAME) } - -pub fn socket(worker_dir_path: &Path) -> PathBuf { - worker_dir_path.join(WORKER_SOCKET_NAME) -} diff --git a/polkadot/node/core/pvf/execute-worker/src/lib.rs b/polkadot/node/core/pvf/execute-worker/src/lib.rs index 9d7bfdf286699..02eaedb96f28e 100644 --- a/polkadot/node/core/pvf/execute-worker/src/lib.rs +++ b/polkadot/node/core/pvf/execute-worker/src/lib.rs @@ -111,6 +111,8 @@ fn send_response(stream: &mut UnixStream, response: Response) -> io::Result<()> /// /// # Parameters /// +/// - `socket_path`: specifies the path to the socket used to communicate with the host. +/// /// - `worker_dir_path`: specifies the path to the worker-specific temporary directory. /// /// - `node_version`: if `Some`, is checked against the `worker_version`. A mismatch results in @@ -121,6 +123,7 @@ fn send_response(stream: &mut UnixStream, response: Response) -> io::Result<()> /// /// - `security_status`: contains the detected status of security features. pub fn worker_entrypoint( + socket_path: PathBuf, worker_dir_path: PathBuf, node_version: Option<&str>, worker_version: Option<&str>, @@ -128,6 +131,7 @@ pub fn worker_entrypoint( ) { worker_event_loop( WorkerKind::Execute, + socket_path, worker_dir_path, node_version, worker_version, diff --git a/polkadot/node/core/pvf/prepare-worker/src/lib.rs b/polkadot/node/core/pvf/prepare-worker/src/lib.rs index a24f5024722bb..fcc7f6754a7e2 100644 --- a/polkadot/node/core/pvf/prepare-worker/src/lib.rs +++ b/polkadot/node/core/pvf/prepare-worker/src/lib.rs @@ -87,6 +87,8 @@ fn send_response(stream: &mut UnixStream, result: PrepareResult) -> io::Result<( /// /// # Parameters /// +/// - `socket_path`: specifies the path to the socket used to communicate with the host. +/// /// - `worker_dir_path`: specifies the path to the worker-specific temporary directory. /// /// - `node_version`: if `Some`, is checked against the `worker_version`. A mismatch results in @@ -116,6 +118,7 @@ fn send_response(stream: &mut UnixStream, result: PrepareResult) -> io::Result<( /// 7. Send the result of preparation back to the host. If any error occurred in the above steps, we /// send that in the `PrepareResult`. pub fn worker_entrypoint( + socket_path: PathBuf, worker_dir_path: PathBuf, node_version: Option<&str>, worker_version: Option<&str>, @@ -123,6 +126,7 @@ pub fn worker_entrypoint( ) { worker_event_loop( WorkerKind::Prepare, + socket_path, worker_dir_path, node_version, worker_version, diff --git a/polkadot/node/core/pvf/src/worker_intf.rs b/polkadot/node/core/pvf/src/worker_intf.rs index 9825506ba88f6..bd85d84055ce5 100644 --- a/polkadot/node/core/pvf/src/worker_intf.rs +++ b/polkadot/node/core/pvf/src/worker_intf.rs @@ -20,7 +20,7 @@ use crate::LOG_TARGET; use futures::FutureExt as _; use futures_timer::Delay; use pin_project::pin_project; -use polkadot_node_core_pvf_common::{worker_dir, SecurityStatus}; +use polkadot_node_core_pvf_common::SecurityStatus; use rand::Rng; use std::{ fmt, mem, @@ -67,71 +67,99 @@ pub async fn spawn_with_program_path( ) -> Result<(IdleWorker, WorkerHandle), SpawnErr> { let program_path = program_path.into(); let worker_dir = WorkerDir::new(debug_id, cache_path).await?; - let socket_path = worker_dir::socket(&worker_dir.path); - let extra_args: Vec<String> = extra_args.iter().map(|arg| arg.to_string()).collect(); - let listener = UnixListener::bind(&socket_path).map_err(|err| { - gum::warn!( - target: LOG_TARGET, - %debug_id, - ?program_path, - ?extra_args, - ?worker_dir, - ?socket_path, - "cannot bind unix socket: {:?}", - err, - ); - SpawnErr::Bind - })?; - - let handle = WorkerHandle::spawn(&program_path, &extra_args, &worker_dir.path, security_status) - .map_err(|err| { - gum::warn!( - target: LOG_TARGET, - %debug_id, - ?program_path, - ?extra_args, - ?worker_dir.path, - ?socket_path, - "cannot spawn a worker: {:?}", - err, - ); - SpawnErr::ProcessSpawn - })?; - - let worker_dir_path = worker_dir.path.clone(); - futures::select! { - accept_result = listener.accept().fuse() => { - let (stream, _) = accept_result.map_err(|err| { + with_transient_socket_path(debug_id, |socket_path| { + let socket_path = socket_path.to_owned(); + + async move { + let listener = UnixListener::bind(&socket_path).map_err(|err| { gum::warn!( target: LOG_TARGET, %debug_id, ?program_path, ?extra_args, - ?worker_dir_path, + ?worker_dir, ?socket_path, - "cannot accept a worker: {:?}", + "cannot bind unix socket: {:?}", err, ); - SpawnErr::Accept + SpawnErr::Bind })?; - Ok((IdleWorker { stream, pid: handle.id(), worker_dir }, handle)) - } - _ = Delay::new(spawn_timeout).fuse() => { - gum::warn!( - target: LOG_TARGET, - %debug_id, - ?program_path, - ?extra_args, - ?worker_dir_path, - ?socket_path, - ?spawn_timeout, - "spawning and connecting to socket timed out", - ); - Err(SpawnErr::AcceptTimeout) + + let handle = WorkerHandle::spawn( + &program_path, + &extra_args, + &socket_path, + &worker_dir.path, + security_status, + ) + .map_err(|err| { + gum::warn!( + target: LOG_TARGET, + %debug_id, + ?program_path, + ?extra_args, + ?worker_dir.path, + ?socket_path, + "cannot spawn a worker: {:?}", + err, + ); + SpawnErr::ProcessSpawn + })?; + + let worker_dir_path = worker_dir.path.clone(); + futures::select! { + accept_result = listener.accept().fuse() => { + let (stream, _) = accept_result.map_err(|err| { + gum::warn!( + target: LOG_TARGET, + %debug_id, + ?program_path, + ?extra_args, + ?worker_dir_path, + ?socket_path, + "cannot accept a worker: {:?}", + err, + ); + SpawnErr::Accept + })?; + Ok((IdleWorker { stream, pid: handle.id(), worker_dir }, handle)) + } + _ = Delay::new(spawn_timeout).fuse() => { + gum::warn!( + target: LOG_TARGET, + %debug_id, + ?program_path, + ?extra_args, + ?worker_dir_path, + ?socket_path, + ?spawn_timeout, + "spawning and connecting to socket timed out", + ); + Err(SpawnErr::AcceptTimeout) + } + } } - } + }) + .await +} + +async fn with_transient_socket_path<T, F, Fut>(debug_id: &'static str, f: F) -> Result<T, SpawnErr> +where + F: FnOnce(&Path) -> Fut, + Fut: futures::Future<Output = Result<T, SpawnErr>> + 'static, +{ + let socket_path = tmppath(&format!("pvf-host-{}", debug_id)) + .await + .map_err(|_| SpawnErr::TmpPath)?; + let result = f(&socket_path).await; + + // Best effort to remove the socket file. Under normal circumstances the socket will be removed + // by the worker. We make sure that it is removed here, just in case a failed rendezvous. + let _ = tokio::fs::remove_file(socket_path).await; + + result } /// Returns a path under the given `dir`. The path name will start with the given prefix. @@ -169,7 +197,6 @@ pub async fn tmppath_in(prefix: &str, dir: &Path) -> io::Result<PathBuf> { } /// The same as [`tmppath_in`], but uses [`std::env::temp_dir`] as the directory. -#[cfg(test)] pub async fn tmppath(prefix: &str) -> io::Result<PathBuf> { let temp_dir = PathBuf::from(std::env::temp_dir()); tmppath_in(prefix, &temp_dir).await @@ -234,6 +261,7 @@ impl WorkerHandle { fn spawn( program: impl AsRef<Path>, extra_args: &[String], + socket_path: impl AsRef<Path>, worker_dir_path: impl AsRef<Path>, security_status: SecurityStatus, ) -> io::Result<Self> { @@ -257,6 +285,8 @@ impl WorkerHandle { } let mut child = command .args(extra_args) + .arg("--socket-path") + .arg(socket_path.as_ref().as_os_str()) .arg("--worker-dir-path") .arg(worker_dir_path.as_ref().as_os_str()) .args(&security_args) From d21113c13fd33748f3fe8cc77c14a6c9407b9c38 Mon Sep 17 00:00:00 2001 From: Sebastian Kunert <skunert49@gmail.com> Date: Thu, 5 Oct 2023 18:44:03 +0200 Subject: [PATCH 09/54] Delete full db directory with `purge-chain` subcommand (#1786) Closes #1767 Until now the `purge-chain` command would only remove the `full` subfolder of the db folder. However there is also the `parachains` db that currently remains and can cause problems on node restart. Example wiht old code: ``` polkadot purge-chain --database paritydb --base-path /tmp/some-folder Are you sure to remove "/tmp/some-folder/chains/polkadot/paritydb/full"? [y/N]: y "/tmp/some-folder/chains/polkadot/paritydb/full" removed. ``` In this case `/tmp/some-folder/chains/polkadot/paritydb/parachains` would remain and might cause problem on node restart because of version conflicts as described in #1767. After this PR the whole `/tmp/some-folder/chains/polkadot/paritydb` folder will be deleted. --- polkadot/tests/purge_chain_works.rs | 4 ---- substrate/client/cli/src/commands/purge_chain_cmd.rs | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/polkadot/tests/purge_chain_works.rs b/polkadot/tests/purge_chain_works.rs index 831155fb4d7e1..f5a73e232e0cb 100644 --- a/polkadot/tests/purge_chain_works.rs +++ b/polkadot/tests/purge_chain_works.rs @@ -57,7 +57,6 @@ async fn purge_chain_rocksdb_works() { assert!(cmd.wait().unwrap().success()); assert!(tmpdir.path().join("chains/rococo_dev").exists()); assert!(tmpdir.path().join("chains/rococo_dev/db/full").exists()); - assert!(tmpdir.path().join("chains/rococo_dev/db/full/parachains").exists()); // Purge chain let status = Command::new(cargo_bin("polkadot")) @@ -102,7 +101,6 @@ async fn purge_chain_paritydb_works() { assert!(cmd.wait().unwrap().success()); assert!(tmpdir.path().join("chains/rococo_dev").exists()); assert!(tmpdir.path().join("chains/rococo_dev/paritydb/full").exists()); - assert!(tmpdir.path().join("chains/rococo_dev/paritydb/parachains").exists()); // Purge chain let status = Command::new(cargo_bin("polkadot")) @@ -118,8 +116,6 @@ async fn purge_chain_paritydb_works() { // Make sure that the chain folder exists, but `db/full` is deleted. assert!(tmpdir.path().join("chains/rococo_dev").exists()); assert!(!tmpdir.path().join("chains/rococo_dev/paritydb/full").exists()); - // Parachains removal requires calling "purge-chain --parachains". - assert!(tmpdir.path().join("chains/rococo_dev/paritydb/parachains").exists()); }) .await; } diff --git a/substrate/client/cli/src/commands/purge_chain_cmd.rs b/substrate/client/cli/src/commands/purge_chain_cmd.rs index 2ff3d4b9a04c0..6e7b8143a5bb7 100644 --- a/substrate/client/cli/src/commands/purge_chain_cmd.rs +++ b/substrate/client/cli/src/commands/purge_chain_cmd.rs @@ -48,7 +48,7 @@ pub struct PurgeChainCmd { impl PurgeChainCmd { /// Run the purge command pub fn run(&self, database_config: DatabaseSource) -> error::Result<()> { - let db_path = database_config.path().ok_or_else(|| { + let db_path = database_config.path().and_then(|p| p.parent()).ok_or_else(|| { error::Error::Input("Cannot purge custom database implementation".into()) })?; From 0c592329e9cbb5d68373ac16be4899223b2d8de8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 00:09:10 +0200 Subject: [PATCH 10/54] Bump the known_good_semver group with 1 update (#1752) Bumps the known_good_semver group with 1 update: [clap](https://github.com/clap-rs/clap). <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/clap-rs/clap/releases">clap's releases</a>.</em></p> <blockquote> <h2>v4.4.6</h2> <h2>[4.4.6] - 2023-09-28</h2> <h3>Internal</h3> <ul> <li>Upgrade <code>anstream</code></li> </ul> <h2>v4.4.5</h2> <h2>[4.4.5] - 2023-09-25</h2> <h3>Fixes</h3> <ul> <li><em>(parser)</em> When inferring subcommand <code>name</code> or <code>long_flag</code>, allow ambiguous-looking matches that unambiguously map back to the same command</li> <li><em>(parser)</em> When inferring subcommand <code>long_flag</code>, don't panic</li> <li><em>(assert)</em> Clarify what action is causing a positional that doesn't set values which is especially useful for derive users</li> </ul> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/clap-rs/clap/blob/master/CHANGELOG.md">clap's changelog</a>.</em></p> <blockquote> <h2>[4.4.6] - 2023-09-28</h2> <h3>Internal</h3> <ul> <li>Upgrade <code>anstream</code></li> </ul> <h2>[4.4.5] - 2023-09-25</h2> <h3>Fixes</h3> <ul> <li><em>(parser)</em> When inferring subcommand <code>name</code> or <code>long_flag</code>, allow ambiguous-looking matches that unambiguously map back to the same command</li> <li><em>(parser)</em> When inferring subcommand <code>long_flag</code>, don't panic</li> <li><em>(assert)</em> Clarify what action is causing a positional that doesn't set values which is especially useful for derive users</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/clap-rs/clap/commit/39f5e807af1c08acedbf7343ce9ec379a4308636"><code>39f5e80</code></a> chore: Release</li> <li><a href="https://github.com/clap-rs/clap/commit/a5cb6bb988bbacb02e8cf98b6156c860d0801e08"><code>a5cb6bb</code></a> docs: Update changelog</li> <li><a href="https://github.com/clap-rs/clap/commit/418c0017a654e9859adfa9b051815f20e4583e31"><code>418c001</code></a> Merge pull request <a href="https://redirect.github.com/clap-rs/clap/issues/5146">#5146</a> from epage/update</li> <li><a href="https://github.com/clap-rs/clap/commit/485b957c4b90aa010276f813dbd429e1071f8fd9"><code>485b957</code></a> chore: Upgrade anstream</li> <li><a href="https://github.com/clap-rs/clap/commit/a1af8d9ad8f81eb2d71203b50c370a78ce3ec9f3"><code>a1af8d9</code></a> chore: Update from '_rust/main'</li> <li><a href="https://github.com/clap-rs/clap/commit/ac51f0925003597dec21529538597dbd7872d1ac"><code>ac51f09</code></a> chore(ci): Normalize json5 syntax</li> <li><a href="https://github.com/clap-rs/clap/commit/86c29dea384c7392a2b682fa0150f52c0f4c7f00"><code>86c29de</code></a> chore(ci): Updaet Renovate schema</li> <li><a href="https://github.com/clap-rs/clap/commit/204552890d316ec9ae0b21f85298ba1d5d0786f8"><code>2045528</code></a> chore: Release</li> <li><a href="https://github.com/clap-rs/clap/commit/55d223001682bc668f5e4db91afd5e76c2a36597"><code>55d2230</code></a> docs: Update changelog</li> <li><a href="https://github.com/clap-rs/clap/commit/492ee03b325ff98c7702295e024576b52b71358d"><code>492ee03</code></a> Merge pull request <a href="https://redirect.github.com/clap-rs/clap/issues/5140">#5140</a> from epage/dyn</li> <li>Additional commits viewable in <a href="https://github.com/clap-rs/clap/compare/v4.4.4...v4.4.6">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 70 +++++++++---------- cumulus/client/cli/Cargo.toml | 2 +- cumulus/parachain-template/node/Cargo.toml | 2 +- cumulus/polkadot-parachain/Cargo.toml | 2 +- cumulus/test/service/Cargo.toml | 2 +- polkadot/cli/Cargo.toml | 2 +- polkadot/node/malus/Cargo.toml | 2 +- .../test-parachains/adder/collator/Cargo.toml | 2 +- .../undying/collator/Cargo.toml | 2 +- polkadot/utils/generate-bags/Cargo.toml | 2 +- .../remote-ext-tests/bags-list/Cargo.toml | 2 +- substrate/bin/node-template/node/Cargo.toml | 2 +- substrate/bin/node/bench/Cargo.toml | 2 +- substrate/bin/node/cli/Cargo.toml | 4 +- substrate/bin/node/inspect/Cargo.toml | 2 +- .../bin/utils/chain-spec-builder/Cargo.toml | 2 +- substrate/bin/utils/subkey/Cargo.toml | 2 +- substrate/client/cli/Cargo.toml | 2 +- substrate/client/storage-monitor/Cargo.toml | 2 +- .../solution-type/fuzzer/Cargo.toml | 2 +- .../npos-elections/fuzzer/Cargo.toml | 2 +- .../ci/node-template-release/Cargo.toml | 2 +- .../utils/frame/benchmarking-cli/Cargo.toml | 2 +- .../frame/frame-utilities-cli/Cargo.toml | 2 +- .../generate-bags/node-runtime/Cargo.toml | 2 +- .../utils/frame/try-runtime/cli/Cargo.toml | 2 +- 26 files changed, 61 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 46f6b6ea1fc9c..340587d268d7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,9 +229,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ "anstyle", "anstyle-parse", @@ -267,9 +267,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", "windows-sys 0.48.0", @@ -2303,7 +2303,7 @@ name = "chain-spec-builder" version = "2.0.0" dependencies = [ "ansi_term", - "clap 4.4.4", + "clap 4.4.6", "node-cli", "rand 0.8.5", "sc-chain-spec", @@ -2433,9 +2433,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.4" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" dependencies = [ "clap_builder", "clap_derive 4.4.2", @@ -2443,9 +2443,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.4" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" dependencies = [ "anstream", "anstyle", @@ -2459,7 +2459,7 @@ version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "586a385f7ef2f8b4d86bddaa0c094794e7ccbfe5ffef1f434fe928143fc783a5" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", ] [[package]] @@ -3031,7 +3031,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.4.4", + "clap 4.4.6", "criterion-plot", "futures", "is-terminal", @@ -3196,7 +3196,7 @@ dependencies = [ name = "cumulus-client-cli" version = "0.1.0" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "parity-scale-codec", "sc-chain-spec", "sc-cli", @@ -3881,7 +3881,7 @@ name = "cumulus-test-service" version = "0.1.0" dependencies = [ "async-trait", - "clap 4.4.4", + "clap 4.4.6", "criterion 0.5.1", "cumulus-client-cli", "cumulus-client-consensus-common", @@ -5088,7 +5088,7 @@ dependencies = [ "Inflector", "array-bytes", "chrono", - "clap 4.4.4", + "clap 4.4.6", "comfy-table", "frame-benchmarking", "frame-support", @@ -5180,7 +5180,7 @@ dependencies = [ name = "frame-election-solution-type-fuzzer" version = "2.0.0-alpha.5" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "frame-election-provider-solution-type", "frame-election-provider-support", "frame-support", @@ -8074,7 +8074,7 @@ name = "node-bench" version = "0.9.0-dev" dependencies = [ "array-bytes", - "clap 4.4.4", + "clap 4.4.6", "derive_more", "fs_extra", "futures", @@ -8111,7 +8111,7 @@ version = "3.0.0-dev" dependencies = [ "array-bytes", "assert_cmd", - "clap 4.4.4", + "clap 4.4.6", "clap_complete", "criterion 0.4.0", "frame-benchmarking-cli", @@ -8237,7 +8237,7 @@ dependencies = [ name = "node-inspect" version = "0.9.0-dev" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "parity-scale-codec", "sc-cli", "sc-client-api", @@ -8291,7 +8291,7 @@ dependencies = [ name = "node-runtime-generate-bags" version = "3.0.0" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "generate-bags", "kitchensink-runtime", ] @@ -8300,7 +8300,7 @@ dependencies = [ name = "node-template" version = "4.0.0-dev" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "frame-benchmarking", "frame-benchmarking-cli", "frame-system", @@ -8343,7 +8343,7 @@ dependencies = [ name = "node-template-release" version = "3.0.0" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "flate2", "fs_extra", "glob", @@ -10756,7 +10756,7 @@ dependencies = [ name = "parachain-template-node" version = "0.1.0" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "color-print", "cumulus-client-cli", "cumulus-client-collator", @@ -11493,7 +11493,7 @@ dependencies = [ name = "polkadot-cli" version = "1.1.0" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "frame-benchmarking-cli", "futures", "log", @@ -12315,7 +12315,7 @@ dependencies = [ "bridge-hub-kusama-runtime", "bridge-hub-polkadot-runtime", "bridge-hub-rococo-runtime", - "clap 4.4.4", + "clap 4.4.6", "collectives-polkadot-runtime", "color-print", "contracts-rococo-runtime", @@ -12792,7 +12792,7 @@ version = "1.0.0" dependencies = [ "assert_matches", "async-trait", - "clap 4.4.4", + "clap 4.4.6", "color-eyre", "futures", "futures-timer", @@ -12939,7 +12939,7 @@ dependencies = [ name = "polkadot-voter-bags" version = "1.0.0" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "generate-bags", "sp-io", "westend-runtime", @@ -13699,7 +13699,7 @@ checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" name = "remote-ext-tests-bags-list" version = "1.0.0" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "frame-system", "log", "pallet-bags-list-remote-tests", @@ -14411,7 +14411,7 @@ version = "0.10.0-dev" dependencies = [ "array-bytes", "chrono", - "clap 4.4.4", + "clap 4.4.6", "fdlimit", "futures", "futures-timer", @@ -15514,7 +15514,7 @@ dependencies = [ name = "sc-storage-monitor" version = "0.1.0" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "fs4", "log", "sc-client-db", @@ -17055,7 +17055,7 @@ dependencies = [ name = "sp-npos-elections-fuzzer" version = "2.0.0-alpha.5" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "honggfuzz", "rand 0.8.5", "sp-npos-elections", @@ -17659,7 +17659,7 @@ dependencies = [ name = "subkey" version = "3.0.0" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "sc-cli", ] @@ -17701,7 +17701,7 @@ dependencies = [ name = "substrate-frame-cli" version = "4.0.0-dev" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "frame-support", "frame-system", "sc-cli", @@ -18160,7 +18160,7 @@ dependencies = [ name = "test-parachain-adder-collator" version = "1.0.0" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "futures", "futures-timer", "log", @@ -18208,7 +18208,7 @@ dependencies = [ name = "test-parachain-undying-collator" version = "1.0.0" dependencies = [ - "clap 4.4.4", + "clap 4.4.6", "futures", "futures-timer", "log", @@ -18854,7 +18854,7 @@ version = "0.10.0-dev" dependencies = [ "assert_cmd", "async-trait", - "clap 4.4.4", + "clap 4.4.6", "frame-remote-externalities", "frame-try-runtime", "hex", diff --git a/cumulus/client/cli/Cargo.toml b/cumulus/client/cli/Cargo.toml index c45b669fc6d1e..5dd18f0c156d1 100644 --- a/cumulus/client/cli/Cargo.toml +++ b/cumulus/client/cli/Cargo.toml @@ -5,7 +5,7 @@ authors.workspace = true edition.workspace = true [dependencies] -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } codec = { package = "parity-scale-codec", version = "3.0.0" } url = "2.4.0" diff --git a/cumulus/parachain-template/node/Cargo.toml b/cumulus/parachain-template/node/Cargo.toml index 114b25d126115..e73c7b507262e 100644 --- a/cumulus/parachain-template/node/Cargo.toml +++ b/cumulus/parachain-template/node/Cargo.toml @@ -11,7 +11,7 @@ build = "build.rs" publish = false [dependencies] -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } log = "0.4.20" codec = { package = "parity-scale-codec", version = "3.0.0" } serde = { version = "1.0.188", features = ["derive"] } diff --git a/cumulus/polkadot-parachain/Cargo.toml b/cumulus/polkadot-parachain/Cargo.toml index 9d5a5ecda1e68..778b056b89d15 100644 --- a/cumulus/polkadot-parachain/Cargo.toml +++ b/cumulus/polkadot-parachain/Cargo.toml @@ -12,7 +12,7 @@ path = "src/main.rs" [dependencies] async-trait = "0.1.73" -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } codec = { package = "parity-scale-codec", version = "3.0.0" } futures = "0.3.28" hex-literal = "0.4.1" diff --git a/cumulus/test/service/Cargo.toml b/cumulus/test/service/Cargo.toml index 5285376f3d59c..c996a01a12ed1 100644 --- a/cumulus/test/service/Cargo.toml +++ b/cumulus/test/service/Cargo.toml @@ -11,7 +11,7 @@ path = "src/main.rs" [dependencies] async-trait = "0.1.73" -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } codec = { package = "parity-scale-codec", version = "3.0.0" } criterion = { version = "0.5.1", features = [ "async_tokio" ] } jsonrpsee = { version = "0.16.2", features = ["server"] } diff --git a/polkadot/cli/Cargo.toml b/polkadot/cli/Cargo.toml index 4646fb1c58822..8057342aaea0c 100644 --- a/polkadot/cli/Cargo.toml +++ b/polkadot/cli/Cargo.toml @@ -15,7 +15,7 @@ wasm-opt = false crate-type = ["cdylib", "rlib"] [dependencies] -clap = { version = "4.4.4", features = ["derive"], optional = true } +clap = { version = "4.4.6", features = ["derive"], optional = true } log = "0.4.17" thiserror = "1.0.48" futures = "0.3.21" diff --git a/polkadot/node/malus/Cargo.toml b/polkadot/node/malus/Cargo.toml index 42dd4af73c130..9ce725f168221 100644 --- a/polkadot/node/malus/Cargo.toml +++ b/polkadot/node/malus/Cargo.toml @@ -40,7 +40,7 @@ assert_matches = "1.5" async-trait = "0.1.57" sp-keystore = { path = "../../../substrate/primitives/keystore" } sp-core = { path = "../../../substrate/primitives/core" } -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } futures = "0.3.21" futures-timer = "3.0.2" gum = { package = "tracing-gum", path = "../gum" } diff --git a/polkadot/parachain/test-parachains/adder/collator/Cargo.toml b/polkadot/parachain/test-parachains/adder/collator/Cargo.toml index 73b1fab529ef4..70f2ae769a8f4 100644 --- a/polkadot/parachain/test-parachains/adder/collator/Cargo.toml +++ b/polkadot/parachain/test-parachains/adder/collator/Cargo.toml @@ -13,7 +13,7 @@ path = "src/main.rs" [dependencies] parity-scale-codec = { version = "3.6.1", default-features = false, features = ["derive"] } -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } futures = "0.3.21" futures-timer = "3.0.2" log = "0.4.17" diff --git a/polkadot/parachain/test-parachains/undying/collator/Cargo.toml b/polkadot/parachain/test-parachains/undying/collator/Cargo.toml index 3fbed4046bded..4569d4e153b19 100644 --- a/polkadot/parachain/test-parachains/undying/collator/Cargo.toml +++ b/polkadot/parachain/test-parachains/undying/collator/Cargo.toml @@ -13,7 +13,7 @@ path = "src/main.rs" [dependencies] parity-scale-codec = { version = "3.6.1", default-features = false, features = ["derive"] } -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } futures = "0.3.21" futures-timer = "3.0.2" log = "0.4.17" diff --git a/polkadot/utils/generate-bags/Cargo.toml b/polkadot/utils/generate-bags/Cargo.toml index 873b0c0030ade..95ca57ea728e1 100644 --- a/polkadot/utils/generate-bags/Cargo.toml +++ b/polkadot/utils/generate-bags/Cargo.toml @@ -6,7 +6,7 @@ edition.workspace = true license.workspace = true [dependencies] -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } generate-bags = { path = "../../../substrate/utils/frame/generate-bags" } sp-io = { path = "../../../substrate/primitives/io" } diff --git a/polkadot/utils/remote-ext-tests/bags-list/Cargo.toml b/polkadot/utils/remote-ext-tests/bags-list/Cargo.toml index c59d354dc4a96..e305edc039b5a 100644 --- a/polkadot/utils/remote-ext-tests/bags-list/Cargo.toml +++ b/polkadot/utils/remote-ext-tests/bags-list/Cargo.toml @@ -15,6 +15,6 @@ sp-tracing = { path = "../../../../substrate/primitives/tracing" } frame-system = { path = "../../../../substrate/frame/system" } sp-core = { path = "../../../../substrate/primitives/core" } -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } log = "0.4.17" tokio = { version = "1.24.2", features = ["macros"] } diff --git a/substrate/bin/node-template/node/Cargo.toml b/substrate/bin/node-template/node/Cargo.toml index 35654e7d56499..23840cce2229b 100644 --- a/substrate/bin/node-template/node/Cargo.toml +++ b/substrate/bin/node-template/node/Cargo.toml @@ -17,7 +17,7 @@ targets = ["x86_64-unknown-linux-gnu"] name = "node-template" [dependencies] -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } futures = { version = "0.3.21", features = ["thread-pool"]} sc-cli = { path = "../../../client/cli" } diff --git a/substrate/bin/node/bench/Cargo.toml b/substrate/bin/node/bench/Cargo.toml index 33560bca4923d..c111d345623d5 100644 --- a/substrate/bin/node/bench/Cargo.toml +++ b/substrate/bin/node/bench/Cargo.toml @@ -13,7 +13,7 @@ publish = false [dependencies] array-bytes = "6.1" -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } log = "0.4.17" node-primitives = { path = "../primitives" } node-testing = { path = "../testing" } diff --git a/substrate/bin/node/cli/Cargo.toml b/substrate/bin/node/cli/Cargo.toml index c47f8a5c3e52f..5ce4c73f98c4f 100644 --- a/substrate/bin/node/cli/Cargo.toml +++ b/substrate/bin/node/cli/Cargo.toml @@ -38,7 +38,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] # third-party dependencies array-bytes = "6.1" -clap = { version = "4.4.4", features = ["derive"], optional = true } +clap = { version = "4.4.6", features = ["derive"], optional = true } codec = { package = "parity-scale-codec", version = "3.6.1" } serde = { version = "1.0.188", features = ["derive"] } jsonrpsee = { version = "0.16.2", features = ["server"] } @@ -135,7 +135,7 @@ pallet-timestamp = { path = "../../../frame/timestamp" } substrate-cli-test-utils = { path = "../../../test-utils/cli" } [build-dependencies] -clap = { version = "4.4.4", optional = true } +clap = { version = "4.4.6", optional = true } clap_complete = { version = "4.0.2", optional = true } node-inspect = { path = "../inspect", optional = true} frame-benchmarking-cli = { path = "../../../utils/frame/benchmarking-cli", optional = true} diff --git a/substrate/bin/node/inspect/Cargo.toml b/substrate/bin/node/inspect/Cargo.toml index 06e9674117e68..4a92db2918589 100644 --- a/substrate/bin/node/inspect/Cargo.toml +++ b/substrate/bin/node/inspect/Cargo.toml @@ -13,7 +13,7 @@ publish = false targets = ["x86_64-unknown-linux-gnu"] [dependencies] -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } codec = { package = "parity-scale-codec", version = "3.6.1" } thiserror = "1.0" sc-cli = { path = "../../../client/cli" } diff --git a/substrate/bin/utils/chain-spec-builder/Cargo.toml b/substrate/bin/utils/chain-spec-builder/Cargo.toml index f564ff19af0f3..c7690faf7d065 100644 --- a/substrate/bin/utils/chain-spec-builder/Cargo.toml +++ b/substrate/bin/utils/chain-spec-builder/Cargo.toml @@ -22,7 +22,7 @@ crate-type = ["rlib"] [dependencies] ansi_term = "0.12.1" -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } rand = "0.8" node-cli = { path = "../../node/cli" } sc-chain-spec = { path = "../../../client/chain-spec" } diff --git a/substrate/bin/utils/subkey/Cargo.toml b/substrate/bin/utils/subkey/Cargo.toml index 4e8cb606c94e5..6606d8ac365f9 100644 --- a/substrate/bin/utils/subkey/Cargo.toml +++ b/substrate/bin/utils/subkey/Cargo.toml @@ -17,5 +17,5 @@ path = "src/main.rs" name = "subkey" [dependencies] -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } sc-cli = { path = "../../../client/cli" } diff --git a/substrate/client/cli/Cargo.toml b/substrate/client/cli/Cargo.toml index cfdcb39b1fa79..b78287be890fc 100644 --- a/substrate/client/cli/Cargo.toml +++ b/substrate/client/cli/Cargo.toml @@ -15,7 +15,7 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] array-bytes = "6.1" chrono = "0.4.27" -clap = { version = "4.4.4", features = ["derive", "string"] } +clap = { version = "4.4.6", features = ["derive", "string"] } fdlimit = "0.2.1" futures = "0.3.21" libp2p-identity = { version = "0.1.3", features = ["peerid", "ed25519"]} diff --git a/substrate/client/storage-monitor/Cargo.toml b/substrate/client/storage-monitor/Cargo.toml index 7538f5ba602ce..021ee76240b98 100644 --- a/substrate/client/storage-monitor/Cargo.toml +++ b/substrate/client/storage-monitor/Cargo.toml @@ -9,7 +9,7 @@ description = "Storage monitor service for substrate" homepage = "https://substrate.io" [dependencies] -clap = { version = "4.4.4", features = ["derive", "string"] } +clap = { version = "4.4.6", features = ["derive", "string"] } log = "0.4.17" fs4 = "0.6.3" sc-client-db = { path = "../db", default-features = false} diff --git a/substrate/frame/election-provider-support/solution-type/fuzzer/Cargo.toml b/substrate/frame/election-provider-support/solution-type/fuzzer/Cargo.toml index 6ac09dd45c601..e485920145416 100644 --- a/substrate/frame/election-provider-support/solution-type/fuzzer/Cargo.toml +++ b/substrate/frame/election-provider-support/solution-type/fuzzer/Cargo.toml @@ -13,7 +13,7 @@ publish = false targets = ["x86_64-unknown-linux-gnu"] [dependencies] -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } honggfuzz = "0.5" rand = { version = "0.8", features = ["std", "small_rng"] } diff --git a/substrate/primitives/npos-elections/fuzzer/Cargo.toml b/substrate/primitives/npos-elections/fuzzer/Cargo.toml index eeb9deebb71e9..5e75f926f87ca 100644 --- a/substrate/primitives/npos-elections/fuzzer/Cargo.toml +++ b/substrate/primitives/npos-elections/fuzzer/Cargo.toml @@ -14,7 +14,7 @@ publish = false targets = ["x86_64-unknown-linux-gnu"] [dependencies] -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } honggfuzz = "0.5" rand = { version = "0.8", features = ["std", "small_rng"] } sp-npos-elections = { path = ".." } diff --git a/substrate/scripts/ci/node-template-release/Cargo.toml b/substrate/scripts/ci/node-template-release/Cargo.toml index c0e0275872467..73ffce8645b86 100644 --- a/substrate/scripts/ci/node-template-release/Cargo.toml +++ b/substrate/scripts/ci/node-template-release/Cargo.toml @@ -11,7 +11,7 @@ publish = false targets = ["x86_64-unknown-linux-gnu"] [dependencies] -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } flate2 = "1.0" fs_extra = "1.3" glob = "0.3" diff --git a/substrate/utils/frame/benchmarking-cli/Cargo.toml b/substrate/utils/frame/benchmarking-cli/Cargo.toml index 9ba22e24faacd..e32fe47b72971 100644 --- a/substrate/utils/frame/benchmarking-cli/Cargo.toml +++ b/substrate/utils/frame/benchmarking-cli/Cargo.toml @@ -15,7 +15,7 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] array-bytes = "6.1" chrono = "0.4" -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } codec = { package = "parity-scale-codec", version = "3.6.1" } comfy-table = { version = "7.0.1", default-features = false } handlebars = "4.2.2" diff --git a/substrate/utils/frame/frame-utilities-cli/Cargo.toml b/substrate/utils/frame/frame-utilities-cli/Cargo.toml index 5a3365dc90038..24c04f47391e8 100644 --- a/substrate/utils/frame/frame-utilities-cli/Cargo.toml +++ b/substrate/utils/frame/frame-utilities-cli/Cargo.toml @@ -11,7 +11,7 @@ documentation = "https://docs.rs/substrate-frame-cli" readme = "README.md" [dependencies] -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } frame-support = { path = "../../../frame/support" } frame-system = { path = "../../../frame/system" } sc-cli = { path = "../../../client/cli" } diff --git a/substrate/utils/frame/generate-bags/node-runtime/Cargo.toml b/substrate/utils/frame/generate-bags/node-runtime/Cargo.toml index e1490aa363ca7..13e6113835623 100644 --- a/substrate/utils/frame/generate-bags/node-runtime/Cargo.toml +++ b/substrate/utils/frame/generate-bags/node-runtime/Cargo.toml @@ -14,4 +14,4 @@ kitchensink-runtime = { path = "../../../../bin/node/runtime" } generate-bags = { path = ".." } # third-party -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } diff --git a/substrate/utils/frame/try-runtime/cli/Cargo.toml b/substrate/utils/frame/try-runtime/cli/Cargo.toml index 3f693ca6c82d8..65380a22ce6e0 100644 --- a/substrate/utils/frame/try-runtime/cli/Cargo.toml +++ b/substrate/utils/frame/try-runtime/cli/Cargo.toml @@ -35,7 +35,7 @@ frame-try-runtime = { path = "../../../../frame/try-runtime", optional = true} substrate-rpc-client = { path = "../../rpc/client" } async-trait = "0.1.57" -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive"] } hex = { version = "0.4.3", default-features = false } log = "0.4.17" parity-scale-codec = "3.6.1" From 1835c091c42456e8df3ecbf0a94b7b88c395f623 Mon Sep 17 00:00:00 2001 From: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Date: Fri, 6 Oct 2023 10:08:09 +0200 Subject: [PATCH 11/54] Revive Substrate Crate (#1477) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/paritytech/polkadot-sdk/issues/1450 Bringing back the Substrate crate that was forgotten in the monorepo import 😅. It is a doc-only crate. Version number is set to `1.0.0` and publishing is enabled (so that we can link to docs.rs). --------- Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Co-authored-by: Bastian Köcher <git@kchr.de> Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> --- Cargo.lock | 19 +++ Cargo.toml | 25 ++-- substrate/Cargo.toml | 28 ++++ substrate/src/src/lib.rs | 297 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 357 insertions(+), 12 deletions(-) create mode 100644 substrate/Cargo.toml create mode 100644 substrate/src/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 340587d268d7e..e925c445b6b05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17663,6 +17663,25 @@ dependencies = [ "sc-cli", ] +[[package]] +name = "substrate" +version = "1.0.0" +dependencies = [ + "aquamarine", + "chain-spec-builder", + "frame-support", + "sc-cli", + "sc-consensus-aura", + "sc-consensus-babe", + "sc-consensus-beefy", + "sc-consensus-grandpa", + "sc-consensus-manual-seal", + "sc-consensus-pow", + "sc-service", + "sp-runtime", + "subkey", +] + [[package]] name = "substrate-bip39" version = "0.4.4" diff --git a/Cargo.toml b/Cargo.toml index 9c936024e5fad..aa0b93737f7ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,11 +110,11 @@ members = [ "polkadot/node/core/parachains-inherent", "polkadot/node/core/prospective-parachains", "polkadot/node/core/provisioner", + "polkadot/node/core/pvf-checker", "polkadot/node/core/pvf", "polkadot/node/core/pvf/common", "polkadot/node/core/pvf/execute-worker", "polkadot/node/core/pvf/prepare-worker", - "polkadot/node/core/pvf-checker", "polkadot/node/core/runtime-api", "polkadot/node/gum", "polkadot/node/gum/proc-macro", @@ -134,10 +134,10 @@ members = [ "polkadot/node/overseer", "polkadot/node/primitives", "polkadot/node/service", - "polkadot/node/subsystem", "polkadot/node/subsystem-test-helpers", "polkadot/node/subsystem-types", "polkadot/node/subsystem-util", + "polkadot/node/subsystem", "polkadot/node/test/client", "polkadot/node/test/service", "polkadot/node/zombienet-backchannel", @@ -165,8 +165,8 @@ members = [ "polkadot/utils/generate-bags", "polkadot/utils/remote-ext-tests/bags-list", "polkadot/xcm", - "polkadot/xcm/pallet-xcm", "polkadot/xcm/pallet-xcm-benchmarks", + "polkadot/xcm/pallet-xcm", "polkadot/xcm/procedural", "polkadot/xcm/xcm-builder", "polkadot/xcm/xcm-executor", @@ -174,6 +174,10 @@ members = [ "polkadot/xcm/xcm-simulator", "polkadot/xcm/xcm-simulator/example", "polkadot/xcm/xcm-simulator/fuzzer", + "substrate", + "substrate/bin/node-template/node", + "substrate/bin/node-template/pallets/template", + "substrate/bin/node-template/runtime", "substrate/bin/node/bench", "substrate/bin/node/cli", "substrate/bin/node/executor", @@ -182,9 +186,6 @@ members = [ "substrate/bin/node/rpc", "substrate/bin/node/runtime", "substrate/bin/node/testing", - "substrate/bin/node-template/node", - "substrate/bin/node-template/pallets/template", - "substrate/bin/node-template/runtime", "substrate/bin/utils/chain-spec-builder", "substrate/bin/utils/subkey", "substrate/client/allocator", @@ -216,6 +217,7 @@ members = [ "substrate/client/keystore", "substrate/client/merkle-mountain-range", "substrate/client/merkle-mountain-range/rpc", + "substrate/client/network-gossip", "substrate/client/network", "substrate/client/network/bitswap", "substrate/client/network/common", @@ -224,13 +226,12 @@ members = [ "substrate/client/network/sync", "substrate/client/network/test", "substrate/client/network/transactions", - "substrate/client/network-gossip", "substrate/client/offchain", "substrate/client/proposer-metrics", - "substrate/client/rpc", "substrate/client/rpc-api", "substrate/client/rpc-servers", "substrate/client/rpc-spec-v2", + "substrate/client/rpc", "substrate/client/service", "substrate/client/service/test", "substrate/client/state-db", @@ -257,8 +258,8 @@ members = [ "substrate/frame/bags-list/fuzzer", "substrate/frame/bags-list/remote-tests", "substrate/frame/balances", - "substrate/frame/beefy", "substrate/frame/beefy-mmr", + "substrate/frame/beefy", "substrate/frame/benchmarking", "substrate/frame/benchmarking/pov", "substrate/frame/bounties", @@ -398,12 +399,12 @@ members = [ "substrate/primitives/offchain", "substrate/primitives/panic-handler", "substrate/primitives/rpc", - "substrate/primitives/runtime", "substrate/primitives/runtime-interface", "substrate/primitives/runtime-interface/proc-macro", - "substrate/primitives/runtime-interface/test", - "substrate/primitives/runtime-interface/test-wasm", "substrate/primitives/runtime-interface/test-wasm-deprecated", + "substrate/primitives/runtime-interface/test-wasm", + "substrate/primitives/runtime-interface/test", + "substrate/primitives/runtime", "substrate/primitives/session", "substrate/primitives/staking", "substrate/primitives/state-machine", diff --git a/substrate/Cargo.toml b/substrate/Cargo.toml new file mode 100644 index 0000000000000..d77f02c606031 --- /dev/null +++ b/substrate/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "substrate" +description = "Next-generation framework for blockchain innovation" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +homepage = "https://substrate.io" +repository.workspace = true +authors.workspace = true +edition.workspace = true +version = "1.0.0" + +# The dependencies are only needed for docs. +[dependencies] +aquamarine = "0.3.2" + +subkey = { path = "bin/utils/subkey" } +chain-spec-builder = { path = "bin/utils/chain-spec-builder" } + +sc-service = { path = "client/service" } +sc-cli = { path = "client/cli" } +sc-consensus-aura = { path = "client/consensus/aura" } +sc-consensus-babe = { path = "client/consensus/babe" } +sc-consensus-grandpa = { path = "client/consensus/grandpa" } +sc-consensus-beefy = { path = "client/consensus/beefy" } +sc-consensus-manual-seal = { path = "client/consensus/manual-seal" } +sc-consensus-pow = { path = "client/consensus/pow" } + +sp-runtime = { path = "primitives/runtime" } +frame-support = { path = "frame/support" } diff --git a/substrate/src/src/lib.rs b/substrate/src/src/lib.rs new file mode 100644 index 0000000000000..16a6067789657 --- /dev/null +++ b/substrate/src/src/lib.rs @@ -0,0 +1,297 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! # Substrate +//! +//! Substrate is a Rust framework for building blockchains in a modular and extensible way. While in +//! itself un-opinionated, it is the main engine behind the Polkadot ecosystem. +//! +//! [![github]](https://github.com/paritytech/substrate/) - [![polkadot]](https://polkadot.network) +//! +//! This crate in itself does not contain any code and is just meant ot be a documentation hub for +//! substrate-based crates. +//! +//! ## Overview +//! +//! Substrate approaches blockchain development with an acknowledgement of a few self-evident +//! truths: +//! +//! 1. Society and technology evolves. +//! 2. Humans are fallible. +//! +//! This, specifically, makes the task of designing a correct, safe and long-lasting blockchain +//! system hard. +//! +//! Nonetheless, in order to achieve this goal, substrate embraces the following: +//! +//! 1. Use of **Rust** as a modern, and safe programming language, which limits human error through +//! various means, most notably memory safety. +//! 2. Substrate is written from the ground-up with a generic, modular and extensible design. This +//! ensures that software components can be easily swapped and upgraded. Examples of this is +//! multiple consensus mechanisms provided by Substrate, as listed below. +//! 3. Lastly, the final blockchain system created with the above properties needs to be +//! upgradeable. In order to achieve this, Substrate is designed as a meta-protocol, whereby the +//! application logic of the blockchain (called "Runtime") is encoded as a Wasm blob, and is +//! stored onchain. The rest of the system (called "Client") acts as the executor of the Wasm +//! blob. +//! +//! In essence, the meta-protocol of all Substrate based chains is the "Runtime as Wasm blob" +//! accord. This enables the Runtime to become inherently upgradeable (without forks). The upgrade +//! is merely a matter of the Wasm blob being changed in the chain state, which is, in principle, +//! same as updating an account's balance. +//! +//! To learn more about the substrate architecture using some visuals, see [`substrate_diagram`]. +//! +//! `FRAME`, Substrate's default runtime development library takes the above even further by +//! embracing a declarative programming model whereby correctness is enhanced and the system is +//! highly configurable through parameterization. +//! +//! All in all, this design enables all substrate-based chains to achieve forkless, self-enacting +//! upgrades out of the box. Combined with governance abilities that are shipped with `FRAME`, this +//! enables a chain to survive the test of time. +//! +//! ## How to Get Stared +//! +//! Most developers want to leave the client side code as-is, and focus on the runtime. To do so, +//! look into the [`frame_support`] crate, which is the entry point crate into runtime development +//! with FRAME. +//! +//! > Side note, it is entirely possible to craft a substrate-based runtime without FRAME, an +//! > example of which can be found [here](https://github.com/JoshOrndorff/frameless-node-template). +//! +//! In more broad terms, the following avenues exist into developing with substrate: +//! +//! * **Templates**: A number of substrate-based templates exist and they can be used for various +//! purposes, with zero to little additional code needed. All of these templates contain runtimes +//! that are highly configurable and are likely suitable for basic needs. +//! * `FRAME`: If need, one can customize that runtime even further, by using `FRAME` and developing +//! custom modules. +//! * **Core**: To the contrary, some developers may want to customize the client side software to +//! achieve novel goals such as a new consensus engine, or a new database backend. While +//! Substrate's main configurability is in the runtime, the client is also highly generic and can +//! be customized to a great extent. +//! +//! ## Structure +//! +//! Substrate is a massive cargo workspace with hundreds of crates, therefore it is useful to know +//! how to navigate its crates. +//! +//! In broad terms, it is divided into three categories: +//! +//! * `sc-*` (short for *substrate-client*) crates, located under `./client` folder. These are all +//! the client crates. Notable examples are crates such as [`sc-network`], various consensus +//! crates, [`sc-rpc-api`] and [`sc-client-db`], all of which are expected to reside in the client +//! side. +//! * `sp-*` (short for *substrate-primitives*) crates, located under `./primitives` folder. These +//! are the traits that glue the client and runtime together, but are not opinionated about what +//! framework is using for building the runtime. Notable examples are [`sp-api`] and [`sp-io`], +//! which form the communication bridge between the client and runtime, as explained in +//! [`substrate_diagram`]. +//! * `pallet-*` and `frame-*` crates, located under `./frame` folder. These are the crates related +//! to FRAME. See [`frame_support`] for more information. +//! +//! ### Wasm Build +//! +//! Many of the Substrate crates, such as entire `sp-*`, need to compile to both Wasm (when a Wasm +//! runtime is being generated) and native (for example, when testing). To achieve this, Substrate +//! follows the convention of the Rust community, and uses a `feature = "std"` to signify that a +//! crate is being built with the standard library, and is built for native. Otherwise, it is built +//! for `no_std`. +//! +//! This can be summarized in `#![cfg_attr(not(feature = "std"), no_std)]`, which you can often find +//! in any Substrate-based runtime. +//! +//! Substrate-based runtimes use [`substrate-wasm-builder`] in their `build.rs` to automatically +//! build their Wasm files as a part of normal build commandsOnce built, the wasm file is placed in +//! `./target/{debug|release}/wbuild/{runtime_name}.wasm`. +//! +//! ### Binaries +//! +//! Multiple binaries are shipped with substrate, the most important of which are located in the +//! `./bin` folder. +//! +//! * [`node`] is an extensive substrate node that contains the superset of all runtime and client +//! side features. The corresponding runtime, called [`kitchensink_runtime`] contains all of the +//! modules that are provided with `FRAME`. This node and runtime is only used for testing and +//! demonstration. +//! * [`chain-spec-builder`]: Utility to build more detailed chain-specs for the aforementioned +//! node. Other projects typically contain a `build-spec` subcommand that does the same. +//! * [`node-template`]: a template node that contains a minimal set of features and can act as a +//! starting point of a project. +//! * [`subkey`]: Substrate's key management utility. +//! +//! ### Anatomy of a Binary Crate +//! +//! From the above, [`node`] and [`node-template`] are essentially blueprints of a substrate-based +//! project, as the name of the latter is implying. Each substrate-based project typically contains +//! the following: +//! +//! * Under `./runtime`, a `./runtime/src/lib.rs` which is the top level runtime amalgamator file. +//! This file typically contains the [`frame_support::construct_runtime`] macro, which is the +//! final definition of a runtime. +//! +//! * Under `./node`, a `main.rs`, which is the point, and a `./service.rs`, which contains all the +//! client side components. Skimming this file yields an overview of the networking, database, +//! consensus and similar client side components. +//! +//! > The above two are conventions, not rules. +//! +//! ## Parachain? +//! +//! As noted above, Substrate is the main engine behind the Polkadot ecosystem. One of the ways +//! through which Polkadot can be utilized is by building "parachains", blockchains that are +//! connected to Polkadot's shared security. +//! +//! To build a parachain, one could use [`Cumulus`](https://github.com/paritytech/cumulus/), the +//! library on top of Substrate, empowering any substrate-based chain to be a Polkadot parachain. +//! +//! ## Where To Go Next? +//! +//! Additional noteworthy crates within substrate: +//! +//! - RPC APIs of a Substrate node: [`sc-rpc-api`]/[`sc-rpc`] +//! - CLI Options of a Substrate node: [`sc-cli`] +//! - All of the consensus related crates provided by Substrate: +//! - [`sc-consensus-aura`] +//! - [`sc-consensus-babe`] +//! - [`sc-consensus-grandpa`] +//! - [`sc-consensus-beefy`] +//! - [`sc-consensus-manual-seal`] +//! - [`sc-consensus-pow`] +//! +//! Additional noteworthy external resources: +//! +//! - [Substrate Developer Hub](https://substrate.dev) +//! - [Parity Tech's Documentation Hub](https://paritytech.github.io/) +//! - [Frontier: Substrate's Ethereum Compatibility Library](https://paritytech.github.io/frontier/) +//! - [Polkadot Wiki](https://wiki.polkadot.network/en/) +//! +//! Notable upstream crates: +//! +//! - [`parity-scale-codec`](https://github.com/paritytech/parity-scale-codec) +//! - [`parity-db`](https://github.com/paritytech/parity-db) +//! - [`trie`](https://github.com/paritytech/trie) +//! - [`parity-common`](https://github.com/paritytech/parity-common) +//! +//! Templates: +//! +//! - classic [`substrate-node-template`](https://github.com/substrate-developer-hub/substrate-node-template) +//! - classic [cumulus-parachain-template](https://github.com/substrate-developer-hub/substrate-parachain-template) +//! - [`extended-parachain-template`](https://github.com/paritytech/extended-parachain-template) +//! - [`frontier-parachain-template`](https://github.com/paritytech/frontier-parachain-template) +//! +//! [polkadot]: +//! https://img.shields.io/badge/polkadot-E6007A?style=for-the-badge&logo=polkadot&logoColor=white +//! [github]: +//! https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github +//! [`sp-io`]: ../sp_io/index.html +//! [`sp-api`]: ../sp_api/index.html +//! [`sp-api`]: ../sp_api/index.html +//! [`sc-client-db`]: ../sc_client_db/index.html +//! [`sc-network`]: ../sc_network/index.html +//! [`sc-rpc-api`]: ../sc_rpc_api/index.html +//! [`sc-rpc`]: ../sc_rpc/index.html +//! [`sc-cli`]: ../sc_cli/index.html +//! [`sc-consensus-aura`]: ../sc_consensus_aura/index.html +//! [`sc-consensus-babe`]: ../sc_consensus_babe/index.html +//! [`sc-consensus-grandpa`]: ../sc_consensus_grandpa/index.html +//! [`sc-consensus-beefy`]: ../sc_consensus_beefy/index.html +//! [`sc-consensus-manual-seal`]: ../sc_consensus_manual_seal/index.html +//! [`sc-consensus-pow`]: ../sc_consensus_pow/index.html +//! [`node`]: ../node_cli/index.html +//! [`node-template`]: ../node_template/index.html +//! [`kitchensink_runtime`]: ../kitchensink_runtime/index.html +//! [`subkey`]: ../subkey/index.html +//! [`chain-spec-builder`]: ../chain_spec_builder/index.html +//! [`substrate-wasm-builder`]: https://crates.io/crates/substrate-wasm-builder + +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(rustdoc::private_intra_doc_links)] + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// In this module, we explore substrate at a more depth. First, let's establish substrate being +/// divided into a client and runtime. +/// +/// ```mermaid +/// graph TB +/// subgraph Substrate +/// direction LR +/// subgraph Client +/// end +/// subgraph Runtime +/// end +/// end +/// ``` +/// +/// The client and the runtime of course need to communicate. This is done through two concepts: +/// +/// 1. Host functions: a way for the (Wasm) runtime to talk to the client. All host functions are +/// defined in [`sp-io`]. For example, [`sp-io::storage`] are the set of host functions that +/// allow the runtime to read and write data to the on-chain state. +/// 2. Runtime APIs: a way for the client to talk to the Wasm runtime. Runtime APIs are defined +/// using macros and utilities in [`sp-api`]. For example, [`sp-api::Core`] is the most basic +/// runtime API that any blockchain must implement in order to be able to (re) execute blocks. +/// +/// ```mermaid +/// graph TB +/// subgraph Substrate +/// direction LR +/// subgraph Client +/// end +/// subgraph Runtime +/// end +/// Client --runtime-api--> Runtime +/// Runtime --host-functions--> Client +/// end +/// ``` +/// +/// Finally, let's expand the diagram a bit further and look at the internals of each component: +/// +/// ```mermaid +/// graph TB +/// subgraph Substrate +/// direction LR +/// subgraph Client +/// Database +/// Networking +/// Consensus +/// end +/// subgraph Runtime +/// subgraph FRAME +/// direction LR +/// Governance +/// Currency +/// Staking +/// Identity +/// end +/// end +/// Client --runtime-api--> Runtime +/// Runtime --host-functions--> Client +/// end +/// ``` +/// +/// As noted the runtime contains all of the application specific logic of the blockchain. This is +/// usually written with `FRAME`. The client, on the other hand, contains reusable and generic +/// components that are not specific to one single blockchain, such as networking, database, and the +/// consensus engine. +/// +/// [`sp-io`]: ../../sp_io/index.html +/// [`sp-api`]: ../../sp_api/index.html +/// [`sp-io::storage`]: ../../sp_io/storage/index.html +/// [`sp-api::Core`]: ../../sp_api/trait.Core.html +pub mod substrate_diagram {} From ddf5e5c04c149f335f2274b9e920e411e83749a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 18:07:01 +0200 Subject: [PATCH 12/54] Bump the known_good_semver group with 1 update (#1802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the known_good_semver group with 1 update: [syn](https://github.com/dtolnay/syn). <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/dtolnay/syn/releases">syn's releases</a>.</em></p> <blockquote> <h2>2.0.38</h2> <ul> <li>Fix <em>"method 'peek' has an incompatible type for trait"</em> error when defining <code>bool</code> as a custom keyword (<a href="https://redirect.github.com/dtolnay/syn/issues/1518">#1518</a>, thanks <a href="https://github.com/Vanille-N"><code>@Vanille-N</code></a>)</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/dtolnay/syn/commit/43632bfb6c78ee1f952645a268ab1ac4af162977"><code>43632bf</code></a> Release 2.0.38</li> <li><a href="https://github.com/dtolnay/syn/commit/abd2c214b44da64a5e420d72919308300eebc23d"><code>abd2c21</code></a> Merge pull request <a href="https://redirect.github.com/dtolnay/syn/issues/1518">#1518</a> from Vanille-N/master</li> <li><a href="https://github.com/dtolnay/syn/commit/6701e6077e15013ef34b15e3ffdae2657e499d83"><code>6701e60</code></a> Absolute path to <code>bool</code> in <code>custom_punctuation.rs</code></li> <li><a href="https://github.com/dtolnay/syn/commit/7313d242398111423f046386aa0a75548f63d236"><code>7313d24</code></a> Resolve single_match_else pedantic clippy lint in code generator</li> <li><a href="https://github.com/dtolnay/syn/commit/67ab64f3c09e17b23493c7cda498e7edb8830f21"><code>67ab64f</code></a> Include unexpected token in the test failure message</li> <li><a href="https://github.com/dtolnay/syn/commit/137ae33486de3f2652487f8f64436ad1429df496"><code>137ae33</code></a> Check no remaining token after the first literal</li> <li><a href="https://github.com/dtolnay/syn/commit/258e9e8a11d188c1ee1ffb2b069819239999f9ac"><code>258e9e8</code></a> Ignore single_match_else pedantic clippy lint in test</li> <li><a href="https://github.com/dtolnay/syn/commit/92fd50ee8cb52968d9c66fbe6d67638c1f838e26"><code>92fd50e</code></a> Test docs.rs documentation build in CI</li> <li>See full diff in <a href="https://github.com/dtolnay/syn/compare/2.0.37...2.0.38">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 100 +++++++++--------- .../parachain-system/proc-macro/Cargo.toml | 2 +- polkadot/node/gum/proc-macro/Cargo.toml | 2 +- polkadot/xcm/procedural/Cargo.toml | 2 +- substrate/client/chain-spec/derive/Cargo.toml | 2 +- .../client/tracing/proc-macro/Cargo.toml | 2 +- .../frame/contracts/proc-macro/Cargo.toml | 2 +- .../solution-type/Cargo.toml | 2 +- .../frame/staking/reward-curve/Cargo.toml | 2 +- substrate/frame/support/procedural/Cargo.toml | 2 +- .../frame/support/procedural/tools/Cargo.toml | 2 +- .../procedural/tools/derive/Cargo.toml | 2 +- .../primitives/api/proc-macro/Cargo.toml | 2 +- .../core/hashing/proc-macro/Cargo.toml | 2 +- substrate/primitives/debug-derive/Cargo.toml | 2 +- .../runtime-interface/proc-macro/Cargo.toml | 2 +- .../primitives/version/proc-macro/Cargo.toml | 2 +- 17 files changed, 66 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e925c445b6b05..1559ae2018065 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1091,7 +1091,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1113,7 +1113,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1130,7 +1130,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1304,7 +1304,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -2484,7 +2484,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -3520,7 +3520,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -4003,7 +4003,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -4043,7 +4043,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -4060,7 +4060,7 @@ checksum = "50c49547d73ba8dcfd4ad7325d64c6d5391ff4224d498fc39a6f3f49825a530d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -4359,7 +4359,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -4421,7 +4421,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.37", + "syn 2.0.38", "termcolor", "toml 0.7.6", "walkdir", @@ -4642,7 +4642,7 @@ checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -4653,7 +4653,7 @@ checksum = "c2ad8cef1d801a4686bfd8919f0b30eac4c8e48968c437a6405ded4fb5272d2b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -4798,7 +4798,7 @@ dependencies = [ "fs-err", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -5154,7 +5154,7 @@ dependencies = [ "quote", "scale-info", "sp-arithmetic", - "syn 2.0.37", + "syn 2.0.38", "trybuild", ] @@ -5307,7 +5307,7 @@ dependencies = [ "proc-macro2", "quote", "sp-core-hashing", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -5318,7 +5318,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -5327,7 +5327,7 @@ version = "3.0.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -5550,7 +5550,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -7551,7 +7551,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -7565,7 +7565,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -7576,7 +7576,7 @@ checksum = "c12469fc165526520dff2807c2975310ab47cf7190a45b99b49a7dc8befab17b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -7587,7 +7587,7 @@ checksum = "b8fb85ec1620619edf2984a7693497d4ec88a9665d8b87e942856884c92dbf2a" dependencies = [ "macro_magic_core", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -9295,7 +9295,7 @@ version = "4.0.0-dev" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -10366,7 +10366,7 @@ dependencies = [ "proc-macro2", "quote", "sp-runtime", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -11233,7 +11233,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -11274,7 +11274,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -13117,7 +13117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" dependencies = [ "proc-macro2", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -13199,7 +13199,7 @@ checksum = "3d1eaa7fa0aa1929ffdf7eeb6eac234dde6268914a14ad44d23521ab6a9b258e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -13245,7 +13245,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -13636,7 +13636,7 @@ checksum = "7f7473c2cfcf90008193dd0e3e16599455cb601a9fce322b5bb55de799664925" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -14402,7 +14402,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -15614,7 +15614,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -15974,7 +15974,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -16040,7 +16040,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -16473,7 +16473,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -16873,7 +16873,7 @@ version = "9.0.0" dependencies = [ "quote", "sp-core-hashing", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -16917,7 +16917,7 @@ version = "8.0.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -17148,7 +17148,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -17388,7 +17388,7 @@ dependencies = [ "proc-macro2", "quote", "sp-version", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -18069,9 +18069,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.37" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", @@ -18316,7 +18316,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -18496,7 +18496,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -18677,7 +18677,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -18720,7 +18720,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -19269,7 +19269,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", "wasm-bindgen-shared", ] @@ -19303,7 +19303,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -20444,7 +20444,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -20563,7 +20563,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] diff --git a/cumulus/pallets/parachain-system/proc-macro/Cargo.toml b/cumulus/pallets/parachain-system/proc-macro/Cargo.toml index a1510847fc2f5..cb5d9904c7cf3 100644 --- a/cumulus/pallets/parachain-system/proc-macro/Cargo.toml +++ b/cumulus/pallets/parachain-system/proc-macro/Cargo.toml @@ -9,7 +9,7 @@ description = "Proc macros provided by the parachain-system pallet" proc-macro = true [dependencies] -syn = "2.0.37" +syn = "2.0.38" proc-macro2 = "1.0.64" quote = "1.0.33" proc-macro-crate = "1.3.1" diff --git a/polkadot/node/gum/proc-macro/Cargo.toml b/polkadot/node/gum/proc-macro/Cargo.toml index 83d064cadbed3..1ffaf6160ba2b 100644 --- a/polkadot/node/gum/proc-macro/Cargo.toml +++ b/polkadot/node/gum/proc-macro/Cargo.toml @@ -13,7 +13,7 @@ targets = ["x86_64-unknown-linux-gnu"] proc-macro = true [dependencies] -syn = { version = "2.0.37", features = ["full", "extra-traits"] } +syn = { version = "2.0.38", features = ["full", "extra-traits"] } quote = "1.0.28" proc-macro2 = "1.0.56" proc-macro-crate = "1.1.3" diff --git a/polkadot/xcm/procedural/Cargo.toml b/polkadot/xcm/procedural/Cargo.toml index 1ff73c64780e6..56df0d94f5860 100644 --- a/polkadot/xcm/procedural/Cargo.toml +++ b/polkadot/xcm/procedural/Cargo.toml @@ -11,5 +11,5 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.56" quote = "1.0.28" -syn = "2.0.37" +syn = "2.0.38" Inflector = "0.11.4" diff --git a/substrate/client/chain-spec/derive/Cargo.toml b/substrate/client/chain-spec/derive/Cargo.toml index 202817438b7d8..74b8b656a4042 100644 --- a/substrate/client/chain-spec/derive/Cargo.toml +++ b/substrate/client/chain-spec/derive/Cargo.toml @@ -18,4 +18,4 @@ proc-macro = true proc-macro-crate = "1.1.3" proc-macro2 = "1.0.56" quote = "1.0.28" -syn = "2.0.37" +syn = "2.0.38" diff --git a/substrate/client/tracing/proc-macro/Cargo.toml b/substrate/client/tracing/proc-macro/Cargo.toml index f18e0aacd3776..b134cbce3ccf4 100644 --- a/substrate/client/tracing/proc-macro/Cargo.toml +++ b/substrate/client/tracing/proc-macro/Cargo.toml @@ -18,4 +18,4 @@ proc-macro = true proc-macro-crate = "1.1.3" proc-macro2 = "1.0.56" quote = { version = "1.0.28", features = ["proc-macro"] } -syn = { version = "2.0.37", features = ["proc-macro", "full", "extra-traits", "parsing"] } +syn = { version = "2.0.38", features = ["proc-macro", "full", "extra-traits", "parsing"] } diff --git a/substrate/frame/contracts/proc-macro/Cargo.toml b/substrate/frame/contracts/proc-macro/Cargo.toml index a04f554406709..3ada9e0c23dd9 100644 --- a/substrate/frame/contracts/proc-macro/Cargo.toml +++ b/substrate/frame/contracts/proc-macro/Cargo.toml @@ -17,7 +17,7 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.56" quote = "1.0.28" -syn = { version = "2.0.37", features = ["full"] } +syn = { version = "2.0.38", features = ["full"] } [dev-dependencies] diff --git a/substrate/frame/election-provider-support/solution-type/Cargo.toml b/substrate/frame/election-provider-support/solution-type/Cargo.toml index 39e535c6c3ee6..f4ea4ef6e361f 100644 --- a/substrate/frame/election-provider-support/solution-type/Cargo.toml +++ b/substrate/frame/election-provider-support/solution-type/Cargo.toml @@ -15,7 +15,7 @@ targets = ["x86_64-unknown-linux-gnu"] proc-macro = true [dependencies] -syn = { version = "2.0.37", features = ["full", "visit"] } +syn = { version = "2.0.38", features = ["full", "visit"] } quote = "1.0.28" proc-macro2 = "1.0.56" proc-macro-crate = "1.1.3" diff --git a/substrate/frame/staking/reward-curve/Cargo.toml b/substrate/frame/staking/reward-curve/Cargo.toml index 484afb6136bf0..0a72599611599 100644 --- a/substrate/frame/staking/reward-curve/Cargo.toml +++ b/substrate/frame/staking/reward-curve/Cargo.toml @@ -18,7 +18,7 @@ proc-macro = true proc-macro-crate = "1.1.3" proc-macro2 = "1.0.56" quote = "1.0.28" -syn = { version = "2.0.37", features = ["full", "visit"] } +syn = { version = "2.0.38", features = ["full", "visit"] } [dev-dependencies] sp-runtime = { path = "../../../primitives/runtime" } diff --git a/substrate/frame/support/procedural/Cargo.toml b/substrate/frame/support/procedural/Cargo.toml index 704f355ff6c80..d2854a2a79f00 100644 --- a/substrate/frame/support/procedural/Cargo.toml +++ b/substrate/frame/support/procedural/Cargo.toml @@ -21,7 +21,7 @@ cfg-expr = "0.15.5" itertools = "0.10.3" proc-macro2 = "1.0.56" quote = "1.0.28" -syn = { version = "2.0.37", features = ["full"] } +syn = { version = "2.0.38", features = ["full"] } frame-support-procedural-tools = { path = "tools" } proc-macro-warning = { version = "0.4.2", default-features = false } macro_magic = { version = "0.4.2", features = ["proc_support"] } diff --git a/substrate/frame/support/procedural/tools/Cargo.toml b/substrate/frame/support/procedural/tools/Cargo.toml index 7589fa353d16a..fd42e18180d39 100644 --- a/substrate/frame/support/procedural/tools/Cargo.toml +++ b/substrate/frame/support/procedural/tools/Cargo.toml @@ -15,5 +15,5 @@ targets = ["x86_64-unknown-linux-gnu"] proc-macro-crate = "1.1.3" proc-macro2 = "1.0.56" quote = "1.0.28" -syn = { version = "2.0.37", features = ["full", "visit", "extra-traits"] } +syn = { version = "2.0.38", features = ["full", "visit", "extra-traits"] } frame-support-procedural-tools-derive = { path = "derive" } diff --git a/substrate/frame/support/procedural/tools/derive/Cargo.toml b/substrate/frame/support/procedural/tools/derive/Cargo.toml index 5bf67d43d06ed..06f8e0f3d537a 100644 --- a/substrate/frame/support/procedural/tools/derive/Cargo.toml +++ b/substrate/frame/support/procedural/tools/derive/Cargo.toml @@ -17,4 +17,4 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.56" quote = { version = "1.0.28", features = ["proc-macro"] } -syn = { version = "2.0.37", features = ["proc-macro", "full", "extra-traits", "parsing"] } +syn = { version = "2.0.38", features = ["proc-macro", "full", "extra-traits", "parsing"] } diff --git a/substrate/primitives/api/proc-macro/Cargo.toml b/substrate/primitives/api/proc-macro/Cargo.toml index de5ddcf9dac09..25c87b5d0a4df 100644 --- a/substrate/primitives/api/proc-macro/Cargo.toml +++ b/substrate/primitives/api/proc-macro/Cargo.toml @@ -17,7 +17,7 @@ proc-macro = true [dependencies] quote = "1.0.28" -syn = { version = "2.0.37", features = ["full", "fold", "extra-traits", "visit"] } +syn = { version = "2.0.38", features = ["full", "fold", "extra-traits", "visit"] } proc-macro2 = "1.0.56" blake2 = { version = "0.10.4", default-features = false } proc-macro-crate = "1.1.3" diff --git a/substrate/primitives/core/hashing/proc-macro/Cargo.toml b/substrate/primitives/core/hashing/proc-macro/Cargo.toml index 64b46ab9c19ef..187b5559b931c 100644 --- a/substrate/primitives/core/hashing/proc-macro/Cargo.toml +++ b/substrate/primitives/core/hashing/proc-macro/Cargo.toml @@ -17,5 +17,5 @@ proc-macro = true [dependencies] quote = "1.0.28" -syn = { version = "2.0.37", features = ["full", "parsing"] } +syn = { version = "2.0.38", features = ["full", "parsing"] } sp-core-hashing = { path = "..", default-features = false} diff --git a/substrate/primitives/debug-derive/Cargo.toml b/substrate/primitives/debug-derive/Cargo.toml index 9d3930ac25720..c97c8a0a3991e 100644 --- a/substrate/primitives/debug-derive/Cargo.toml +++ b/substrate/primitives/debug-derive/Cargo.toml @@ -18,7 +18,7 @@ proc-macro = true [dependencies] quote = "1.0.28" -syn = "2.0.37" +syn = "2.0.38" proc-macro2 = "1.0.56" [features] diff --git a/substrate/primitives/runtime-interface/proc-macro/Cargo.toml b/substrate/primitives/runtime-interface/proc-macro/Cargo.toml index 5569e31c93602..fbc49785ae970 100644 --- a/substrate/primitives/runtime-interface/proc-macro/Cargo.toml +++ b/substrate/primitives/runtime-interface/proc-macro/Cargo.toml @@ -20,4 +20,4 @@ Inflector = "0.11.4" proc-macro-crate = "1.1.3" proc-macro2 = "1.0.56" quote = "1.0.28" -syn = { version = "2.0.37", features = ["full", "visit", "fold", "extra-traits"] } +syn = { version = "2.0.38", features = ["full", "visit", "fold", "extra-traits"] } diff --git a/substrate/primitives/version/proc-macro/Cargo.toml b/substrate/primitives/version/proc-macro/Cargo.toml index cc28b8f176b88..7fce559e3ed63 100644 --- a/substrate/primitives/version/proc-macro/Cargo.toml +++ b/substrate/primitives/version/proc-macro/Cargo.toml @@ -19,7 +19,7 @@ proc-macro = true codec = { package = "parity-scale-codec", version = "3.6.1", features = [ "derive" ] } proc-macro2 = "1.0.56" quote = "1.0.28" -syn = { version = "2.0.37", features = ["full", "fold", "extra-traits", "visit"] } +syn = { version = "2.0.38", features = ["full", "fold", "extra-traits", "visit"] } [dev-dependencies] sp-version = { path = ".." } From 35ed272dade7a339605a9f8df25dd451b199deb9 Mon Sep 17 00:00:00 2001 From: Dmitry Borodin <11879032+Dmitry-Borodin@users.noreply.github.com> Date: Sat, 7 Oct 2023 04:04:00 -0500 Subject: [PATCH 13/54] migrate babe and authorship to use derive-impl (#1790) Moving a babe and authorship pallets to the latest and greatest derive_impl. Part of https://github.com/paritytech/polkadot-sdk/issues/171 --------- Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> --- substrate/frame/authorship/src/lib.rs | 33 +++------------------------ substrate/frame/babe/src/mock.rs | 28 ++++------------------- 2 files changed, 7 insertions(+), 54 deletions(-) diff --git a/substrate/frame/authorship/src/lib.rs b/substrate/frame/authorship/src/lib.rs index a9bd0c38cb67c..56a516894dec2 100644 --- a/substrate/frame/authorship/src/lib.rs +++ b/substrate/frame/authorship/src/lib.rs @@ -97,16 +97,10 @@ mod tests { use super::*; use crate as pallet_authorship; use codec::{Decode, Encode}; - use frame_support::{ - traits::{ConstU32, ConstU64}, - ConsensusEngineId, - }; + use frame_support::{derive_impl, ConsensusEngineId}; use sp_core::H256; use sp_runtime::{ - generic::DigestItem, - testing::Header, - traits::{BlakeTwo256, Header as HeaderT, IdentityLookup}, - BuildStorage, + generic::DigestItem, testing::Header, traits::Header as HeaderT, BuildStorage, }; type Block = frame_system::mocking::MockBlock<Test>; @@ -119,30 +113,9 @@ mod tests { } ); + #[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type Nonce = u64; - type RuntimeCall = RuntimeCall; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup<Self::AccountId>; type Block = Block; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = ConstU64<250>; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = (); - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = ConstU32<16>; } impl pallet::Config for Test { diff --git a/substrate/frame/babe/src/mock.rs b/substrate/frame/babe/src/mock.rs index dbffe9f312e60..a3f755902b598 100644 --- a/substrate/frame/babe/src/mock.rs +++ b/substrate/frame/babe/src/mock.rs @@ -24,7 +24,7 @@ use frame_election_provider_support::{ onchain, SequentialPhragmen, }; use frame_support::{ - parameter_types, + derive_impl, parameter_types, traits::{ConstU128, ConstU32, ConstU64, KeyOwnerProofSystem, OnInitialize}, }; use pallet_session::historical as pallet_session_historical; @@ -32,14 +32,14 @@ use pallet_staking::FixedNominationsQuota; use sp_consensus_babe::{AuthorityId, AuthorityPair, Randomness, Slot, VrfSignature}; use sp_core::{ crypto::{KeyTypeId, Pair, VrfSecret}, - H256, U256, + U256, }; use sp_io; use sp_runtime::{ curve::PiecewiseLinear, impl_opaque_keys, testing::{Digest, DigestItem, Header, TestXt}, - traits::{Header as _, IdentityLookup, OpaqueKeys}, + traits::{Header as _, OpaqueKeys}, BuildStorage, Perbill, }; use sp_staking::{EraIndex, SessionIndex}; @@ -63,30 +63,10 @@ frame_support::construct_runtime!( } ); +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type Nonce = u64; - type RuntimeCall = RuntimeCall; - type Hash = H256; - type Version = (); - type Hashing = sp_runtime::traits::BlakeTwo256; - type AccountId = DummyValidatorId; - type Lookup = IdentityLookup<Self::AccountId>; type Block = Block; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = ConstU64<250>; - type PalletInfo = PalletInfo; type AccountData = pallet_balances::AccountData<u128>; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; } impl<C> frame_system::offchain::SendTransactionTypes<C> for Test From 5a6912606a7724932e68a034998327af17e2038a Mon Sep 17 00:00:00 2001 From: Javier Viola <javier@parity.io> Date: Sat, 7 Oct 2023 18:14:21 +0200 Subject: [PATCH 14/54] chore: bump zombienter version (#1806) Bump zombiente version. This version includes the fixes needed for `mixnet`. Thx! --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ee6b9c9873339..61451a9c46201 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,7 +30,7 @@ variables: RUSTY_CACHIER_COMPRESSION_METHOD: zstd NEXTEST_FAILURE_OUTPUT: immediate-final NEXTEST_SUCCESS_OUTPUT: final - ZOMBIENET_IMAGE: "docker.io/paritytech/zombienet:v1.3.68" + ZOMBIENET_IMAGE: "docker.io/paritytech/zombienet:v1.3.69" DOCKER_IMAGES_VERSION: "${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHORT_SHA}" default: From cb944dc54882222453071fc99f2e5014c51823ef Mon Sep 17 00:00:00 2001 From: Muharem <ismailov.m.h@gmail.com> Date: Sat, 7 Oct 2023 19:32:35 +0200 Subject: [PATCH 15/54] Treasury spends various asset kinds (#1333) ### Summary This PR introduces new dispatchables to the treasury pallet, allowing spends of various asset types. The enhanced features of the treasury pallet, in conjunction with the asset-rate pallet, are set up and enabled for Westend and Rococo. ### Westend and Rococo runtimes. Polkadot/Kusams/Rococo Treasury can accept proposals for `spends` of various asset kinds by specifying the asset's location and ID. #### Treasury Instance New Dispatchables: - `spend(AssetKind, AssetBalance, Beneficiary, Option<ValidFrom>)` - propose and approve a spend; - `payout(SpendIndex)` - payout an approved spend or retry a failed payout - `check_payment(SpendIndex)` - check the status of a payout; - `void_spend(SpendIndex)` - void previously approved spend; > existing spend dispatchable renamed to spend_local in this context, the `AssetKind` parameter contains the asset's location and it's corresponding `asset_id`, for example: `USDT` on `AssetHub`, ``` rust location = MultiLocation(0, X1(Parachain(1000))) asset_id = MultiLocation(0, X2(PalletInstance(50), GeneralIndex(1984))) ``` the `Beneficiary` parameter is a `MultiLocation` in the context of the asset's location, for example ``` rust // the Fellowship salary pallet's location / account FellowshipSalaryPallet = MultiLocation(1, X2(Parachain(1001), PalletInstance(64))) // or custom `AccountId` Alice = MultiLocation(0, AccountId32(network: None, id: [1,...])) ``` the `AssetBalance` represents the amount of the `AssetKind` to be transferred to the `Beneficiary`. For permission checks, the asset amount is converted to the native amount and compared against the maximum spendable amount determined by the commanding spend origin. the `spend` dispatchable allows for batching spends with different `ValidFrom` arguments, enabling milestone-based spending. If the expectations tied to an approved spend are not met, it is possible to void the spend later using the `void_spend` dispatchable. Asset Rate Pallet provides the conversion rate from the `AssetKind` to the native balance. #### Asset Rate Instance Dispatchables: - `create(AssetKind, Rate)` - initialize a conversion rate to the native balance for the given asset - `update(AssetKind, Rate)` - update the conversion rate to the native balance for the given asset - `remove(AssetKind)` - remove an existing conversion rate to the native balance for the given asset the pallet's dispatchables can be executed by the Root or Treasurer origins. ### Treasury Pallet Treasury Pallet can accept proposals for `spends` of various asset kinds and pay them out through the implementation of the `Pay` trait. New Dispatchables: - `spend(Config::AssetKind, AssetBalance, Config::Beneficiary, Option<ValidFrom>)` - propose and approve a spend; - `payout(SpendIndex)` - payout an approved spend or retry a failed payout; - `check_payment(SpendIndex)` - check the status of a payout; - `void_spend(SpendIndex)` - void previously approved spend; > existing spend dispatchable renamed to spend_local The parameters' types of the `spend` dispatchable exposed via the pallet's `Config` and allows to propose and accept a spend of a certain amount. An approved spend can be claimed via the `payout` within the `Config::SpendPeriod`. Clients provide an implementation of the `Pay` trait which can pay an asset of the `AssetKind` to the `Beneficiary` in `AssetBalance` units. The implementation of the Pay trait might not have an immediate final payment status, for example if implemented over `XCM` and the actual transfer happens on a remote chain. The `check_status` dispatchable can be executed to update the spend's payment state and retry the `payout` if the payment has failed. --------- Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> Co-authored-by: command-bot <> --- Cargo.lock | 12 + .../assets/asset-hub-westend/Cargo.toml | 13 + .../assets/asset-hub-westend/src/tests/mod.rs | 1 + .../asset-hub-westend/src/tests/treasury.rs | 126 +++++ .../emulated/common/src/lib.rs | 2 + .../asset-hub-westend/src/xcm_config.rs | 22 +- polkadot/runtime/common/Cargo.toml | 7 + polkadot/runtime/common/src/impls.rs | 106 +++- polkadot/runtime/rococo/Cargo.toml | 6 +- polkadot/runtime/rococo/src/lib.rs | 52 +- polkadot/runtime/rococo/src/weights/mod.rs | 1 + .../rococo/src/weights/pallet_asset_rate.rs | 86 +++ .../rococo/src/weights/pallet_treasury.rs | 150 ++++-- polkadot/runtime/westend/Cargo.toml | 6 +- polkadot/runtime/westend/src/lib.rs | 57 +- polkadot/runtime/westend/src/weights/mod.rs | 1 + .../westend/src/weights/pallet_asset_rate.rs | 86 +++ .../westend/src/weights/pallet_treasury.rs | 207 +++++--- substrate/bin/node/runtime/src/lib.rs | 11 +- substrate/frame/asset-rate/src/lib.rs | 5 + substrate/frame/bounties/src/tests.rs | 23 +- substrate/frame/child-bounties/src/tests.rs | 14 +- substrate/frame/support/src/traits/tokens.rs | 3 +- .../frame/support/src/traits/tokens/misc.rs | 20 + .../frame/support/src/traits/tokens/pay.rs | 35 +- substrate/frame/tips/src/tests.rs | 24 +- substrate/frame/treasury/Cargo.toml | 7 +- substrate/frame/treasury/src/benchmarking.rs | 255 +++++++-- substrate/frame/treasury/src/lib.rs | 496 ++++++++++++++++-- substrate/frame/treasury/src/tests.rs | 426 ++++++++++++++- substrate/frame/treasury/src/weights.rs | 223 ++++++-- substrate/primitives/core/src/crypto.rs | 14 + 32 files changed, 2212 insertions(+), 285 deletions(-) create mode 100644 cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/treasury.rs create mode 100644 polkadot/runtime/rococo/src/weights/pallet_asset_rate.rs create mode 100644 polkadot/runtime/westend/src/weights/pallet_asset_rate.rs diff --git a/Cargo.lock b/Cargo.lock index 1559ae2018065..48ac004a85970 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -840,20 +840,27 @@ version = "1.0.0" dependencies = [ "assert_matches", "asset-hub-westend-runtime", + "cumulus-pallet-dmp-queue", + "cumulus-pallet-parachain-system", "frame-support", "frame-system", "integration-tests-common", "pallet-asset-conversion", + "pallet-asset-rate", "pallet-assets", "pallet-balances", + "pallet-treasury", "pallet-xcm", "parachains-common", "parity-scale-codec", "polkadot-core-primitives", "polkadot-parachain-primitives", + "polkadot-runtime-common", "polkadot-runtime-parachains", "sp-runtime", "staging-xcm", + "staging-xcm-builder", + "staging-xcm-executor", "xcm-emulator", ] @@ -10567,6 +10574,7 @@ dependencies = [ name = "pallet-treasury" version = "4.0.0-dev" dependencies = [ + "docify", "frame-benchmarking", "frame-support", "frame-system", @@ -12485,6 +12493,7 @@ dependencies = [ "impl-trait-for-tuples", "libsecp256k1", "log", + "pallet-asset-rate", "pallet-authorship", "pallet-babe", "pallet-balances", @@ -12519,6 +12528,7 @@ dependencies = [ "sp-staking", "sp-std", "staging-xcm", + "staging-xcm-builder", "static_assertions", ] @@ -13900,6 +13910,7 @@ dependencies = [ "frame-try-runtime", "hex-literal", "log", + "pallet-asset-rate", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", @@ -19941,6 +19952,7 @@ dependencies = [ "frame-try-runtime", "hex-literal", "log", + "pallet-asset-rate", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", diff --git a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/Cargo.toml index af9776cbcd999..bf141dafebf2c 100644 --- a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/Cargo.toml +++ b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/Cargo.toml @@ -18,17 +18,24 @@ frame-system = { path = "../../../../../../substrate/frame/system", default-feat pallet-balances = { path = "../../../../../../substrate/frame/balances", default-features = false} pallet-assets = { path = "../../../../../../substrate/frame/assets", default-features = false} pallet-asset-conversion = { path = "../../../../../../substrate/frame/asset-conversion", default-features = false} +pallet-treasury = { path = "../../../../../../substrate/frame/treasury", default-features = false} +pallet-asset-rate = { path = "../../../../../../substrate/frame/asset-rate", default-features = false} # Polkadot polkadot-core-primitives = { path = "../../../../../../polkadot/core-primitives", default-features = false} polkadot-parachain-primitives = { path = "../../../../../../polkadot/parachain", default-features = false} +polkadot-runtime-common = { path = "../../../../../../polkadot/runtime/common" } polkadot-runtime-parachains = { path = "../../../../../../polkadot/runtime/parachains" } xcm = { package = "staging-xcm", path = "../../../../../../polkadot/xcm", default-features = false} +xcm-builder = { package = "staging-xcm-builder", path = "../../../../../../polkadot/xcm/xcm-builder", default-features = false} +xcm-executor = { package = "staging-xcm-executor", path = "../../../../../../polkadot/xcm/xcm-executor", default-features = false} pallet-xcm = { path = "../../../../../../polkadot/xcm/pallet-xcm", default-features = false} # Cumulus parachains-common = { path = "../../../../common" } asset-hub-westend-runtime = { path = "../../../../runtimes/assets/asset-hub-westend" } +cumulus-pallet-dmp-queue = { default-features = false, path = "../../../../../pallets/dmp-queue" } +cumulus-pallet-parachain-system = { default-features = false, path = "../../../../../pallets/parachain-system" } # Local xcm-emulator = { path = "../../../../../xcm/xcm-emulator", default-features = false} @@ -37,15 +44,21 @@ integration-tests-common = { path = "../../common", default-features = false} [features] runtime-benchmarks = [ "asset-hub-westend-runtime/runtime-benchmarks", + "cumulus-pallet-parachain-system/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "integration-tests-common/runtime-benchmarks", "pallet-asset-conversion/runtime-benchmarks", + "pallet-asset-rate/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", + "pallet-treasury/runtime-benchmarks", "pallet-xcm/runtime-benchmarks", "parachains-common/runtime-benchmarks", "polkadot-parachain-primitives/runtime-benchmarks", + "polkadot-runtime-common/runtime-benchmarks", "polkadot-runtime-parachains/runtime-benchmarks", "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", + "xcm-executor/runtime-benchmarks", ] diff --git a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/mod.rs b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/mod.rs index b3841af0e6c38..0c9de89c5f98f 100644 --- a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/mod.rs +++ b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/mod.rs @@ -18,3 +18,4 @@ mod send; mod set_xcm_versions; mod swap; mod teleport; +mod treasury; diff --git a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/treasury.rs b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/treasury.rs new file mode 100644 index 0000000000000..cf06f58682da2 --- /dev/null +++ b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/treasury.rs @@ -0,0 +1,126 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// 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 crate::*; +use frame_support::traits::fungibles::{Create, Inspect, Mutate}; +use integration_tests_common::constants::accounts::{ALICE, BOB}; +use polkadot_runtime_common::impls::VersionedLocatableAsset; +use xcm_executor::traits::ConvertLocation; + +#[test] +fn create_and_claim_treasury_spend() { + const ASSET_ID: u32 = 1984; + const SPEND_AMOUNT: u128 = 1_000_000; + // treasury location from a sibling parachain. + let treasury_location: MultiLocation = MultiLocation::new(1, PalletInstance(37)); + // treasury account on a sibling parachain. + let treasury_account = + asset_hub_westend_runtime::xcm_config::LocationToAccountId::convert_location( + &treasury_location, + ) + .unwrap(); + let asset_hub_location = MultiLocation::new(0, Parachain(AssetHubWestend::para_id().into())); + let root = <Westend as Chain>::RuntimeOrigin::root(); + // asset kind to be spend from the treasury. + let asset_kind = VersionedLocatableAsset::V3 { + location: asset_hub_location, + asset_id: AssetId::Concrete((PalletInstance(50), GeneralIndex(ASSET_ID.into())).into()), + }; + // treasury spend beneficiary. + let alice: AccountId = Westend::account_id_of(ALICE); + let bob: AccountId = Westend::account_id_of(BOB); + let bob_signed = <Westend as Chain>::RuntimeOrigin::signed(bob.clone()); + + AssetHubWestend::execute_with(|| { + type Assets = <AssetHubWestend as AssetHubWestendPallet>::Assets; + + // create an asset class and mint some assets to the treasury account. + assert_ok!(<Assets as Create<_>>::create( + ASSET_ID, + treasury_account.clone(), + true, + SPEND_AMOUNT / 2 + )); + assert_ok!(<Assets as Mutate<_>>::mint_into(ASSET_ID, &treasury_account, SPEND_AMOUNT * 4)); + // beneficiary has zero balance. + assert_eq!(<Assets as Inspect<_>>::balance(ASSET_ID, &alice,), 0u128,); + }); + + Westend::execute_with(|| { + type RuntimeEvent = <Westend as Chain>::RuntimeEvent; + type Treasury = <Westend as WestendPallet>::Treasury; + type AssetRate = <Westend as WestendPallet>::AssetRate; + + // create a conversion rate from `asset_kind` to the native currency. + assert_ok!(AssetRate::create(root.clone(), Box::new(asset_kind.clone()), 2.into())); + + // create and approve a treasury spend. + assert_ok!(Treasury::spend( + root, + Box::new(asset_kind), + SPEND_AMOUNT, + Box::new(MultiLocation::new(0, Into::<[u8; 32]>::into(alice.clone())).into()), + None, + )); + // claim the spend. + assert_ok!(Treasury::payout(bob_signed.clone(), 0)); + + assert_expected_events!( + Westend, + vec![ + RuntimeEvent::Treasury(pallet_treasury::Event::Paid { .. }) => {}, + ] + ); + }); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent; + type Assets = <AssetHubWestend as AssetHubWestendPallet>::Assets; + + // assert events triggered by xcm pay program + // 1. treasury asset transferred to spend beneficiary + // 2. response to Relay Chain treasury pallet instance sent back + // 3. XCM program completed + assert_expected_events!( + AssetHubWestend, + vec![ + RuntimeEvent::Assets(pallet_assets::Event::Transferred { asset_id: id, from, to, amount }) => { + id: id == &ASSET_ID, + from: from == &treasury_account, + to: to == &alice, + amount: amount == &SPEND_AMOUNT, + }, + RuntimeEvent::ParachainSystem(cumulus_pallet_parachain_system::Event::UpwardMessageSent { .. }) => {}, + RuntimeEvent::DmpQueue(cumulus_pallet_dmp_queue::Event::ExecutedDownward { outcome: Outcome::Complete(..) ,.. }) => {}, + ] + ); + // beneficiary received the assets from the treasury. + assert_eq!(<Assets as Inspect<_>>::balance(ASSET_ID, &alice,), SPEND_AMOUNT,); + }); + + Westend::execute_with(|| { + type RuntimeEvent = <Westend as Chain>::RuntimeEvent; + type Treasury = <Westend as WestendPallet>::Treasury; + + // check the payment status to ensure the response from the AssetHub was received. + assert_ok!(Treasury::check_status(bob_signed, 0)); + assert_expected_events!( + Westend, + vec![ + RuntimeEvent::Treasury(pallet_treasury::Event::SpendProcessed { .. }) => {}, + ] + ); + }); +} diff --git a/cumulus/parachains/integration-tests/emulated/common/src/lib.rs b/cumulus/parachains/integration-tests/emulated/common/src/lib.rs index 2c9581c3a189a..068f0238f4978 100644 --- a/cumulus/parachains/integration-tests/emulated/common/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/common/src/lib.rs @@ -46,6 +46,8 @@ decl_test_relay_chains! { XcmPallet: westend_runtime::XcmPallet, Sudo: westend_runtime::Sudo, Balances: westend_runtime::Balances, + Treasury: westend_runtime::Treasury, + AssetRate: westend_runtime::AssetRate, } }, #[api_version(7)] diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs index 6981c290c98ce..a0921c50dc59b 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs @@ -38,11 +38,12 @@ use xcm::latest::prelude::*; use xcm_builder::{ AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, CurrencyAdapter, - DenyReserveTransferToRelayChain, DenyThenTry, EnsureXcmOrigin, FungiblesAdapter, IsConcrete, - LocalMint, NativeAsset, NoChecking, ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, - SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, - SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, TrailingSetTopicAsId, - UsingComponents, WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, + DenyReserveTransferToRelayChain, DenyThenTry, DescribeFamily, DescribePalletTerminal, + EnsureXcmOrigin, FungiblesAdapter, HashedDescription, IsConcrete, LocalMint, NativeAsset, + NoChecking, ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, SiblingParachainAsNative, + SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, + SovereignSignedViaLocation, TakeWeightCredit, TrailingSetTopicAsId, UsingComponents, + WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, }; use xcm_executor::{traits::WithOriginFilter, XcmExecutor}; @@ -75,6 +76,9 @@ pub type LocationToAccountId = ( SiblingParachainConvertsVia<Sibling, AccountId>, // Straight up local `AccountId32` origins just alias directly to `AccountId`. AccountId32Aliases<RelayNetwork, AccountId>, + // Foreign chain account alias into local accounts according to a hash of their standard + // description. + HashedDescription<AccountId, DescribeFamily<DescribePalletTerminal>>, ); /// Means for transacting the native currency on this chain. @@ -222,6 +226,9 @@ match_types! { MultiLocation { parents: 1, interior: Here } | MultiLocation { parents: 1, interior: X1(Plurality { .. }) } }; + pub type TreasuryPallet: impl Contains<MultiLocation> = { + MultiLocation { parents: 1, interior: X1(PalletInstance(37)) } + }; } /// A call filter for the XCM Transact instruction. This is a temporary measure until we properly @@ -449,8 +456,9 @@ pub type Barrier = TrailingSetTopicAsId< // If the message is one that immediately attemps to pay for execution, then // allow it. AllowTopLevelPaidExecutionFrom<Everything>, - // Parent and its pluralities (i.e. governance bodies) get free execution. - AllowExplicitUnpaidExecutionFrom<ParentOrParentsPlurality>, + // Parent, its pluralities (i.e. governance bodies) and treasury pallet get + // free execution. + AllowExplicitUnpaidExecutionFrom<(ParentOrParentsPlurality, TreasuryPallet)>, // Subscriptions for version tracking are OK. AllowSubscriptionsFrom<Everything>, ), diff --git a/polkadot/runtime/common/Cargo.toml b/polkadot/runtime/common/Cargo.toml index 17617bf4ada3f..2d1aad6a575e7 100644 --- a/polkadot/runtime/common/Cargo.toml +++ b/polkadot/runtime/common/Cargo.toml @@ -38,6 +38,7 @@ pallet-timestamp = { path = "../../../substrate/frame/timestamp", default-featur pallet-vesting = { path = "../../../substrate/frame/vesting", default-features = false } pallet-transaction-payment = { path = "../../../substrate/frame/transaction-payment", default-features = false } pallet-treasury = { path = "../../../substrate/frame/treasury", default-features = false } +pallet-asset-rate = { path = "../../../substrate/frame/asset-rate", default-features = false } pallet-election-provider-multi-phase = { path = "../../../substrate/frame/election-provider-multi-phase", default-features = false } frame-election-provider-support = { path = "../../../substrate/frame/election-provider-support", default-features = false } @@ -50,6 +51,7 @@ runtime-parachains = { package = "polkadot-runtime-parachains", path = "../parac slot-range-helper = { path = "slot_range_helper", default-features = false } xcm = { package = "staging-xcm", path = "../../xcm", default-features = false } +xcm-builder = { package = "staging-xcm-builder", path = "../../xcm/xcm-builder", default-features = false } [dev-dependencies] hex-literal = "0.4.1" @@ -74,6 +76,7 @@ std = [ "inherents/std", "libsecp256k1/std", "log/std", + "pallet-asset-rate/std", "pallet-authorship/std", "pallet-balances/std", "pallet-election-provider-multi-phase/std", @@ -100,6 +103,7 @@ std = [ "sp-session/std", "sp-staking/std", "sp-std/std", + "xcm-builder/std", "xcm/std", ] runtime-benchmarks = [ @@ -109,6 +113,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "libsecp256k1/hmac", "libsecp256k1/static-context", + "pallet-asset-rate/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-election-provider-multi-phase/runtime-benchmarks", @@ -121,12 +126,14 @@ runtime-benchmarks = [ "runtime-parachains/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "sp-staking/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", ] try-runtime = [ "frame-election-provider-support/try-runtime", "frame-support-test/try-runtime", "frame-support/try-runtime", "frame-system/try-runtime", + "pallet-asset-rate/try-runtime", "pallet-authorship/try-runtime", "pallet-babe?/try-runtime", "pallet-balances/try-runtime", diff --git a/polkadot/runtime/common/src/impls.rs b/polkadot/runtime/common/src/impls.rs index 0d0dee2e9ad91..590593745ed04 100644 --- a/polkadot/runtime/common/src/impls.rs +++ b/polkadot/runtime/common/src/impls.rs @@ -18,8 +18,10 @@ use crate::NegativeImbalance; use frame_support::traits::{Currency, Imbalance, OnUnbalanced}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use primitives::Balance; -use sp_runtime::Perquintill; +use sp_runtime::{traits::TryConvert, Perquintill, RuntimeDebug}; +use xcm::VersionedMultiLocation; /// Logic for the author to get a portion of fees. pub struct ToAuthor<R>(sp_std::marker::PhantomData<R>); @@ -98,13 +100,104 @@ pub fn era_payout( (staking_payout, rest) } +/// Versioned locatable asset type which contains both an XCM `location` and `asset_id` to identify +/// an asset which exists on some chain. +#[derive( + Encode, Decode, Eq, PartialEq, Clone, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen, +)] +pub enum VersionedLocatableAsset { + #[codec(index = 3)] + V3 { + /// The (relative) location in which the asset ID is meaningful. + location: xcm::v3::MultiLocation, + /// The asset's ID. + asset_id: xcm::v3::AssetId, + }, +} + +/// Converts the [`VersionedLocatableAsset`] to the [`xcm_builder::LocatableAssetId`]. +pub struct LocatableAssetConverter; +impl TryConvert<VersionedLocatableAsset, xcm_builder::LocatableAssetId> + for LocatableAssetConverter +{ + fn try_convert( + asset: VersionedLocatableAsset, + ) -> Result<xcm_builder::LocatableAssetId, VersionedLocatableAsset> { + match asset { + VersionedLocatableAsset::V3 { location, asset_id } => + Ok(xcm_builder::LocatableAssetId { asset_id, location }), + } + } +} + +/// Converts the [`VersionedMultiLocation`] to the [`xcm::latest::MultiLocation`]. +pub struct VersionedMultiLocationConverter; +impl TryConvert<&VersionedMultiLocation, xcm::latest::MultiLocation> + for VersionedMultiLocationConverter +{ + fn try_convert( + location: &VersionedMultiLocation, + ) -> Result<xcm::latest::MultiLocation, &VersionedMultiLocation> { + let latest = match location.clone() { + VersionedMultiLocation::V2(l) => l.try_into().map_err(|_| location)?, + VersionedMultiLocation::V3(l) => l, + }; + Ok(latest) + } +} + +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmarks { + use super::VersionedLocatableAsset; + use pallet_asset_rate::AssetKindFactory; + use pallet_treasury::ArgumentsFactory as TreasuryArgumentsFactory; + use xcm::prelude::*; + + /// Provides a factory method for the [`VersionedLocatableAsset`]. + /// The location of the asset is determined as a Parachain with an ID equal to the passed seed. + pub struct AssetRateArguments; + impl AssetKindFactory<VersionedLocatableAsset> for AssetRateArguments { + fn create_asset_kind(seed: u32) -> VersionedLocatableAsset { + VersionedLocatableAsset::V3 { + location: xcm::v3::MultiLocation::new(0, X1(Parachain(seed))), + asset_id: xcm::v3::MultiLocation::new( + 0, + X2(PalletInstance(seed.try_into().unwrap()), GeneralIndex(seed.into())), + ) + .into(), + } + } + } + + /// Provide factory methods for the [`VersionedLocatableAsset`] and the `Beneficiary` of the + /// [`VersionedMultiLocation`]. The location of the asset is determined as a Parachain with an + /// ID equal to the passed seed. + pub struct TreasuryArguments; + impl TreasuryArgumentsFactory<VersionedLocatableAsset, VersionedMultiLocation> + for TreasuryArguments + { + fn create_asset_kind(seed: u32) -> VersionedLocatableAsset { + AssetRateArguments::create_asset_kind(seed) + } + fn create_beneficiary(seed: [u8; 32]) -> VersionedMultiLocation { + VersionedMultiLocation::V3(xcm::v3::MultiLocation::new( + 0, + X1(AccountId32 { network: None, id: seed }), + )) + } + } +} + #[cfg(test)] mod tests { use super::*; use frame_support::{ dispatch::DispatchClass, parameter_types, - traits::{ConstU32, FindAuthor}, + traits::{ + tokens::{PayFromAccount, UnityAssetBalanceConversion}, + ConstU32, FindAuthor, + }, weights::Weight, PalletId, }; @@ -189,6 +282,7 @@ mod tests { parameter_types! { pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const MaxApprovals: u32 = 100; + pub TreasuryAccount: AccountId = Treasury::account_id(); } impl pallet_treasury::Config for Test { @@ -208,6 +302,14 @@ mod tests { type MaxApprovals = MaxApprovals; type WeightInfo = (); type SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>; + type AssetKind = (); + type Beneficiary = Self::AccountId; + type BeneficiaryLookup = IdentityLookup<Self::AccountId>; + type Paymaster = PayFromAccount<Balances, TreasuryAccount>; + type BalanceConverter = UnityAssetBalanceConversion; + type PayoutPeriod = ConstU64<0>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); } pub struct OneAuthor; diff --git a/polkadot/runtime/rococo/Cargo.toml b/polkadot/runtime/rococo/Cargo.toml index ab9b2f11acfbe..0b8a8624bb673 100644 --- a/polkadot/runtime/rococo/Cargo.toml +++ b/polkadot/runtime/rococo/Cargo.toml @@ -52,6 +52,7 @@ pallet-collective = { path = "../../../substrate/frame/collective", default-feat pallet-conviction-voting = { path = "../../../substrate/frame/conviction-voting", default-features = false } pallet-democracy = { path = "../../../substrate/frame/democracy", default-features = false } pallet-elections-phragmen = { path = "../../../substrate/frame/elections-phragmen", default-features = false } +pallet-asset-rate = { path = "../../../substrate/frame/asset-rate", default-features = false } frame-executive = { path = "../../../substrate/frame/executive", default-features = false } pallet-grandpa = { path = "../../../substrate/frame/grandpa", default-features = false } pallet-identity = { path = "../../../substrate/frame/identity", default-features = false } @@ -72,7 +73,7 @@ pallet-scheduler = { path = "../../../substrate/frame/scheduler", default-featur pallet-session = { path = "../../../substrate/frame/session", default-features = false } pallet-society = { path = "../../../substrate/frame/society", default-features = false } pallet-sudo = { path = "../../../substrate/frame/sudo", default-features = false } -frame-support = { path = "../../../substrate/frame/support", default-features = false } +frame-support = { path = "../../../substrate/frame/support", default-features = false, features = ["tuples-96"] } pallet-staking = { path = "../../../substrate/frame/staking", default-features = false } frame-system = { path = "../../../substrate/frame/system", default-features = false } frame-system-rpc-runtime-api = { path = "../../../substrate/frame/system/rpc/runtime-api", default-features = false } @@ -131,6 +132,7 @@ std = [ "inherents/std", "log/std", "offchain-primitives/std", + "pallet-asset-rate/std", "pallet-authority-discovery/std", "pallet-authorship/std", "pallet-babe/std", @@ -207,6 +209,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-asset-rate/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-bounties/runtime-benchmarks", @@ -258,6 +261,7 @@ try-runtime = [ "frame-system/try-runtime", "frame-try-runtime", "frame-try-runtime/try-runtime", + "pallet-asset-rate/try-runtime", "pallet-authority-discovery/try-runtime", "pallet-authorship/try-runtime", "pallet-babe/try-runtime", diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 4bdcc1237394e..fd3e656f69573 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -30,7 +30,10 @@ use primitives::{ ValidationCode, ValidationCodeHash, ValidatorId, ValidatorIndex, PARACHAIN_KEY_TYPE_ID, }; use runtime_common::{ - assigned_slots, auctions, claims, crowdloan, impl_runtime_weights, impls::ToAuthor, + assigned_slots, auctions, claims, crowdloan, impl_runtime_weights, + impls::{ + LocatableAssetConverter, ToAuthor, VersionedLocatableAsset, VersionedMultiLocationConverter, + }, paras_registrar, paras_sudo_wrapper, prod_or_fast, slots, BlockHashCount, BlockLength, SlowAdjustingFeeUpdate, }; @@ -79,7 +82,8 @@ use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, traits::{ AccountIdLookup, BlakeTwo256, Block as BlockT, ConstU32, ConvertInto, - Extrinsic as ExtrinsicT, Keccak256, OpaqueKeys, SaturatedConversion, Verify, + Extrinsic as ExtrinsicT, IdentityLookup, Keccak256, OpaqueKeys, SaturatedConversion, + Verify, }, transaction_validity::{TransactionPriority, TransactionSource, TransactionValidity}, ApplyExtrinsicResult, FixedU128, KeyTypeId, Perbill, Percent, Permill, RuntimeDebug, @@ -88,7 +92,11 @@ use sp_staking::SessionIndex; #[cfg(any(feature = "std", test))] use sp_version::NativeVersion; use sp_version::RuntimeVersion; -use xcm::latest::Junction; +use xcm::{ + latest::{InteriorMultiLocation, Junction, Junction::PalletInstance}, + VersionedMultiLocation, +}; +use xcm_builder::PayOverXcm; pub use frame_system::Call as SystemCall; pub use pallet_balances::Call as BalancesCall; @@ -385,6 +393,10 @@ parameter_types! { pub const SpendPeriod: BlockNumber = 6 * DAYS; pub const Burn: Permill = Permill::from_perthousand(2); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); + pub const PayoutSpendPeriod: BlockNumber = 30 * DAYS; + // The asset's interior location for the paying account. This is the Treasury + // pallet instance (which sits at index 18). + pub TreasuryInteriorLocation: InteriorMultiLocation = PalletInstance(18).into(); pub const TipCountdown: BlockNumber = 1 * DAYS; pub const TipFindersFee: Percent = Percent::from_percent(20); @@ -394,6 +406,7 @@ parameter_types! { pub const MaxAuthorities: u32 = 100_000; pub const MaxKeys: u32 = 10_000; pub const MaxPeerInHeartbeats: u32 = 10_000; + pub const MaxBalance: Balance = Balance::max_value(); } impl pallet_treasury::Config for Runtime { @@ -413,6 +426,23 @@ impl pallet_treasury::Config for Runtime { type WeightInfo = weights::pallet_treasury::WeightInfo<Runtime>; type SpendFunds = Bounties; type SpendOrigin = TreasurySpender; + type AssetKind = VersionedLocatableAsset; + type Beneficiary = VersionedMultiLocation; + type BeneficiaryLookup = IdentityLookup<Self::Beneficiary>; + type Paymaster = PayOverXcm< + TreasuryInteriorLocation, + crate::xcm_config::XcmRouter, + crate::XcmPallet, + ConstU32<{ 6 * HOURS }>, + Self::Beneficiary, + Self::AssetKind, + LocatableAssetConverter, + VersionedMultiLocationConverter, + >; + type BalanceConverter = AssetRate; + type PayoutPeriod = PayoutSpendPeriod; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = runtime_common::impls::benchmarks::TreasuryArguments; } parameter_types! { @@ -1202,6 +1232,18 @@ impl pallet_sudo::Config for Runtime { type WeightInfo = weights::pallet_sudo::WeightInfo<Runtime>; } +impl pallet_asset_rate::Config for Runtime { + type WeightInfo = weights::pallet_asset_rate::WeightInfo<Runtime>; + type RuntimeEvent = RuntimeEvent; + type CreateOrigin = EnsureRoot<AccountId>; + type RemoveOrigin = EnsureRoot<AccountId>; + type UpdateOrigin = EnsureRoot<AccountId>; + type Currency = Balances; + type AssetKind = <Runtime as pallet_treasury::Config>::AssetKind; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = runtime_common::impls::benchmarks::AssetRateArguments; +} + construct_runtime! { pub enum Runtime { @@ -1279,6 +1321,9 @@ construct_runtime! { // Preimage registrar. Preimage: pallet_preimage::{Pallet, Call, Storage, Event<T>, HoldReason} = 32, + // Asset rate. + AssetRate: pallet_asset_rate::{Pallet, Call, Storage, Event<T>} = 39, + // Bounties modules. Bounties: pallet_bounties::{Pallet, Call, Storage, Event<T>} = 35, ChildBounties: pallet_child_bounties = 40, @@ -1465,6 +1510,7 @@ mod benches { [pallet_treasury, Treasury] [pallet_utility, Utility] [pallet_vesting, Vesting] + [pallet_asset_rate, AssetRate] [pallet_whitelist, Whitelist] // XCM [pallet_xcm, XcmPallet] diff --git a/polkadot/runtime/rococo/src/weights/mod.rs b/polkadot/runtime/rococo/src/weights/mod.rs index e0c1c4f413515..9c563a67d98b7 100644 --- a/polkadot/runtime/rococo/src/weights/mod.rs +++ b/polkadot/runtime/rococo/src/weights/mod.rs @@ -16,6 +16,7 @@ //! A list of the different weight modules for our runtime. pub mod frame_system; +pub mod pallet_asset_rate; pub mod pallet_balances; pub mod pallet_balances_nis_counterpart_balances; pub mod pallet_bounties; diff --git a/polkadot/runtime/rococo/src/weights/pallet_asset_rate.rs b/polkadot/runtime/rococo/src/weights/pallet_asset_rate.rs new file mode 100644 index 0000000000000..da2d1958cefcf --- /dev/null +++ b/polkadot/runtime/rococo/src/weights/pallet_asset_rate.rs @@ -0,0 +1,86 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see <http://www.gnu.org/licenses/>. + +//! Autogenerated weights for `pallet_asset_rate` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-07-03, STEPS: `50`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `cob`, CPU: `<UNKNOWN>` +//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: Some("rococo-dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/debug/polkadot +// benchmark +// pallet +// --chain=rococo-dev +// --steps=50 +// --repeat=2 +// --pallet=pallet_asset_rate +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./runtime/rococo/src/weights/ +// --header=./file_header.txt + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_asset_rate`. +pub struct WeightInfo<T>(PhantomData<T>); +impl<T: frame_system::Config> pallet_asset_rate::WeightInfo for WeightInfo<T> { + /// Storage: AssetRate ConversionRateToNative (r:1 w:1) + /// Proof: AssetRate ConversionRateToNative (max_values: None, max_size: Some(1237), added: 3712, mode: MaxEncodedLen) + fn create() -> Weight { + // Proof Size summary in bytes: + // Measured: `42` + // Estimated: `4702` + // Minimum execution time: 143_000_000 picoseconds. + Weight::from_parts(155_000_000, 0) + .saturating_add(Weight::from_parts(0, 4702)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: AssetRate ConversionRateToNative (r:1 w:1) + /// Proof: AssetRate ConversionRateToNative (max_values: None, max_size: Some(1237), added: 3712, mode: MaxEncodedLen) + fn update() -> Weight { + // Proof Size summary in bytes: + // Measured: `110` + // Estimated: `4702` + // Minimum execution time: 156_000_000 picoseconds. + Weight::from_parts(172_000_000, 0) + .saturating_add(Weight::from_parts(0, 4702)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: AssetRate ConversionRateToNative (r:1 w:1) + /// Proof: AssetRate ConversionRateToNative (max_values: None, max_size: Some(1237), added: 3712, mode: MaxEncodedLen) + fn remove() -> Weight { + // Proof Size summary in bytes: + // Measured: `110` + // Estimated: `4702` + // Minimum execution time: 150_000_000 picoseconds. + Weight::from_parts(160_000_000, 0) + .saturating_add(Weight::from_parts(0, 4702)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } +} diff --git a/polkadot/runtime/rococo/src/weights/pallet_treasury.rs b/polkadot/runtime/rococo/src/weights/pallet_treasury.rs index 041d976d82570..144e9d5b87238 100644 --- a/polkadot/runtime/rococo/src/weights/pallet_treasury.rs +++ b/polkadot/runtime/rococo/src/weights/pallet_treasury.rs @@ -17,24 +17,24 @@ //! Autogenerated weights for `pallet_treasury` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-05-26, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-07-07, STEPS: `50`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `bm5`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("rococo-dev"), DB CACHE: 1024 +//! HOSTNAME: `cob`, CPU: `<UNKNOWN>` +//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: Some("rococo-dev"), DB CACHE: 1024 // Executed Command: -// ./target/production/polkadot +// ./target/debug/polkadot // benchmark // pallet // --chain=rococo-dev // --steps=50 -// --repeat=20 +// --repeat=2 // --pallet=pallet_treasury // --extrinsic=* -// --execution=wasm // --wasm-execution=compiled -// --header=./file_header.txt +// --heap-pages=4096 // --output=./runtime/rococo/src/weights/ +// --header=./file_header.txt #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -47,13 +47,21 @@ use core::marker::PhantomData; /// Weight functions for `pallet_treasury`. pub struct WeightInfo<T>(PhantomData<T>); impl<T: frame_system::Config> pallet_treasury::WeightInfo for WeightInfo<T> { - fn spend() -> Weight { + /// Storage: Treasury ProposalCount (r:1 w:1) + /// Proof: Treasury ProposalCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Treasury Approvals (r:1 w:1) + /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// Storage: Treasury Proposals (r:0 w:1) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + fn spend_local() -> Weight { // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 204_000 picoseconds. - Weight::from_parts(233_000, 0) - .saturating_add(Weight::from_parts(0, 0)) + // Measured: `42` + // Estimated: `1887` + // Minimum execution time: 177_000_000 picoseconds. + Weight::from_parts(191_000_000, 0) + .saturating_add(Weight::from_parts(0, 1887)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(3)) } /// Storage: Treasury ProposalCount (r:1 w:1) /// Proof: Treasury ProposalCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) @@ -61,10 +69,10 @@ impl<T: frame_system::Config> pallet_treasury::WeightInfo for WeightInfo<T> { /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) fn propose_spend() -> Weight { // Proof Size summary in bytes: - // Measured: `107` + // Measured: `143` // Estimated: `1489` - // Minimum execution time: 27_592_000 picoseconds. - Weight::from_parts(27_960_000, 0) + // Minimum execution time: 354_000_000 picoseconds. + Weight::from_parts(376_000_000, 0) .saturating_add(Weight::from_parts(0, 1489)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(2)) @@ -75,10 +83,10 @@ impl<T: frame_system::Config> pallet_treasury::WeightInfo for WeightInfo<T> { /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) fn reject_proposal() -> Weight { // Proof Size summary in bytes: - // Measured: `265` + // Measured: `301` // Estimated: `3593` - // Minimum execution time: 40_336_000 picoseconds. - Weight::from_parts(41_085_000, 0) + // Minimum execution time: 547_000_000 picoseconds. + Weight::from_parts(550_000_000, 0) .saturating_add(Weight::from_parts(0, 3593)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -90,13 +98,13 @@ impl<T: frame_system::Config> pallet_treasury::WeightInfo for WeightInfo<T> { /// The range of component `p` is `[0, 99]`. fn approve_proposal(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `433 + p * (8 ±0)` + // Measured: `470 + p * (8 ±0)` // Estimated: `3573` - // Minimum execution time: 9_938_000 picoseconds. - Weight::from_parts(12_061_206, 0) + // Minimum execution time: 104_000_000 picoseconds. + Weight::from_parts(121_184_402, 0) .saturating_add(Weight::from_parts(0, 3573)) - // Standard Error: 801 - .saturating_add(Weight::from_parts(26_602, 0).saturating_mul(p.into())) + // Standard Error: 42_854 + .saturating_add(Weight::from_parts(153_112, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -104,10 +112,10 @@ impl<T: frame_system::Config> pallet_treasury::WeightInfo for WeightInfo<T> { /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) fn remove_approval() -> Weight { // Proof Size summary in bytes: - // Measured: `90` + // Measured: `127` // Estimated: `1887` - // Minimum execution time: 7_421_000 picoseconds. - Weight::from_parts(7_620_000, 0) + // Minimum execution time: 80_000_000 picoseconds. + Weight::from_parts(82_000_000, 0) .saturating_add(Weight::from_parts(0, 1887)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -118,26 +126,98 @@ impl<T: frame_system::Config> pallet_treasury::WeightInfo for WeightInfo<T> { /// Proof: Balances InactiveIssuance (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) /// Storage: Treasury Approvals (r:1 w:1) /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) - /// Storage: Treasury Proposals (r:100 w:100) + /// Storage: Treasury Proposals (r:99 w:99) /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) - /// Storage: System Account (r:201 w:201) + /// Storage: System Account (r:199 w:199) /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) /// Storage: Bounties BountyApprovals (r:1 w:1) /// Proof: Bounties BountyApprovals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) - /// The range of component `p` is `[0, 100]`. + /// The range of component `p` is `[0, 99]`. fn on_initialize_proposals(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `296 + p * (251 ±0)` + // Measured: `331 + p * (251 ±0)` // Estimated: `3593 + p * (5206 ±0)` - // Minimum execution time: 62_706_000 picoseconds. - Weight::from_parts(61_351_470, 0) + // Minimum execution time: 887_000_000 picoseconds. + Weight::from_parts(828_616_021, 0) .saturating_add(Weight::from_parts(0, 3593)) - // Standard Error: 32_787 - .saturating_add(Weight::from_parts(37_873_920, 0).saturating_mul(p.into())) + // Standard Error: 695_351 + .saturating_add(Weight::from_parts(566_114_524, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(p.into()))) .saturating_add(T::DbWeight::get().writes(5)) .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(p.into()))) .saturating_add(Weight::from_parts(0, 5206).saturating_mul(p.into())) } + /// Storage: AssetRate ConversionRateToNative (r:1 w:0) + /// Proof: AssetRate ConversionRateToNative (max_values: None, max_size: Some(1237), added: 3712, mode: MaxEncodedLen) + /// Storage: Treasury SpendCount (r:1 w:1) + /// Proof: Treasury SpendCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Treasury Spends (r:0 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(1848), added: 4323, mode: MaxEncodedLen) + fn spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `114` + // Estimated: `4702` + // Minimum execution time: 208_000_000 picoseconds. + Weight::from_parts(222_000_000, 0) + .saturating_add(Weight::from_parts(0, 4702)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: Treasury Spends (r:1 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(1848), added: 4323, mode: MaxEncodedLen) + /// Storage: XcmPallet QueryCounter (r:1 w:1) + /// Proof Skipped: XcmPallet QueryCounter (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Configuration ActiveConfig (r:1 w:0) + /// Proof Skipped: Configuration ActiveConfig (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Dmp DeliveryFeeFactor (r:1 w:0) + /// Proof Skipped: Dmp DeliveryFeeFactor (max_values: None, max_size: None, mode: Measured) + /// Storage: XcmPallet SupportedVersion (r:1 w:0) + /// Proof Skipped: XcmPallet SupportedVersion (max_values: None, max_size: None, mode: Measured) + /// Storage: XcmPallet VersionDiscoveryQueue (r:1 w:1) + /// Proof Skipped: XcmPallet VersionDiscoveryQueue (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: XcmPallet SafeXcmVersion (r:1 w:0) + /// Proof Skipped: XcmPallet SafeXcmVersion (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Dmp DownwardMessageQueues (r:1 w:1) + /// Proof Skipped: Dmp DownwardMessageQueues (max_values: None, max_size: None, mode: Measured) + /// Storage: Dmp DownwardMessageQueueHeads (r:1 w:1) + /// Proof Skipped: Dmp DownwardMessageQueueHeads (max_values: None, max_size: None, mode: Measured) + /// Storage: XcmPallet Queries (r:0 w:1) + /// Proof Skipped: XcmPallet Queries (max_values: None, max_size: None, mode: Measured) + fn payout() -> Weight { + // Proof Size summary in bytes: + // Measured: `737` + // Estimated: `5313` + // Minimum execution time: 551_000_000 picoseconds. + Weight::from_parts(569_000_000, 0) + .saturating_add(Weight::from_parts(0, 5313)) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().writes(6)) + } + /// Storage: Treasury Spends (r:1 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(1848), added: 4323, mode: MaxEncodedLen) + /// Storage: XcmPallet Queries (r:1 w:1) + /// Proof Skipped: XcmPallet Queries (max_values: None, max_size: None, mode: Measured) + fn check_status() -> Weight { + // Proof Size summary in bytes: + // Measured: `442` + // Estimated: `5313` + // Minimum execution time: 245_000_000 picoseconds. + Weight::from_parts(281_000_000, 0) + .saturating_add(Weight::from_parts(0, 5313)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: Treasury Spends (r:1 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(1848), added: 4323, mode: MaxEncodedLen) + fn void_spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `5313` + // Minimum execution time: 147_000_000 picoseconds. + Weight::from_parts(160_000_000, 0) + .saturating_add(Weight::from_parts(0, 5313)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } } diff --git a/polkadot/runtime/westend/Cargo.toml b/polkadot/runtime/westend/Cargo.toml index c9721935c32b4..58a6cc21eecdb 100644 --- a/polkadot/runtime/westend/Cargo.toml +++ b/polkadot/runtime/westend/Cargo.toml @@ -41,10 +41,11 @@ sp-npos-elections = { path = "../../../substrate/primitives/npos-elections", def frame-election-provider-support = { path = "../../../substrate/frame/election-provider-support", default-features = false } frame-executive = { path = "../../../substrate/frame/executive", default-features = false } -frame-support = { path = "../../../substrate/frame/support", default-features = false } +frame-support = { path = "../../../substrate/frame/support", default-features = false, features = ["tuples-96"] } frame-system = { path = "../../../substrate/frame/system", default-features = false } frame-system-rpc-runtime-api = { path = "../../../substrate/frame/system/rpc/runtime-api", default-features = false } westend-runtime-constants = { package = "westend-runtime-constants", path = "constants", default-features = false } +pallet-asset-rate = { path = "../../../substrate/frame/asset-rate", default-features = false } pallet-authority-discovery = { path = "../../../substrate/frame/authority-discovery", default-features = false } pallet-authorship = { path = "../../../substrate/frame/authorship", default-features = false } pallet-babe = { path = "../../../substrate/frame/babe", default-features = false } @@ -143,6 +144,7 @@ std = [ "inherents/std", "log/std", "offchain-primitives/std", + "pallet-asset-rate/std", "pallet-authority-discovery/std", "pallet-authorship/std", "pallet-babe/std", @@ -228,6 +230,7 @@ runtime-benchmarks = [ "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", "hex-literal", + "pallet-asset-rate/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-bags-list/runtime-benchmarks", "pallet-balances/runtime-benchmarks", @@ -283,6 +286,7 @@ try-runtime = [ "frame-system/try-runtime", "frame-try-runtime", "frame-try-runtime/try-runtime", + "pallet-asset-rate/try-runtime", "pallet-authority-discovery/try-runtime", "pallet-authorship/try-runtime", "pallet-babe/try-runtime", diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 6085b6e37455b..d61acf36b4cb8 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -53,9 +53,14 @@ use primitives::{ ValidatorSignature, PARACHAIN_KEY_TYPE_ID, }; use runtime_common::{ - assigned_slots, auctions, crowdloan, elections::OnChainAccuracy, impl_runtime_weights, - impls::ToAuthor, paras_registrar, paras_sudo_wrapper, prod_or_fast, slots, BalanceToU256, - BlockHashCount, BlockLength, CurrencyToVote, SlowAdjustingFeeUpdate, U256ToBalance, + assigned_slots, auctions, crowdloan, + elections::OnChainAccuracy, + impl_runtime_weights, + impls::{ + LocatableAssetConverter, ToAuthor, VersionedLocatableAsset, VersionedMultiLocationConverter, + }, + paras_registrar, paras_sudo_wrapper, prod_or_fast, slots, BalanceToU256, BlockHashCount, + BlockLength, CurrencyToVote, SlowAdjustingFeeUpdate, U256ToBalance, }; use runtime_parachains::{ assigner_parachains as parachains_assigner_parachains, @@ -77,7 +82,7 @@ use sp_runtime::{ generic, impl_opaque_keys, traits::{ AccountIdLookup, BlakeTwo256, Block as BlockT, ConvertInto, Extrinsic as ExtrinsicT, - Keccak256, OpaqueKeys, SaturatedConversion, Verify, + IdentityLookup, Keccak256, OpaqueKeys, SaturatedConversion, Verify, }, transaction_validity::{TransactionPriority, TransactionSource, TransactionValidity}, ApplyExtrinsicResult, FixedU128, KeyTypeId, Perbill, Percent, Permill, @@ -87,7 +92,11 @@ use sp_std::{collections::btree_map::BTreeMap, prelude::*}; #[cfg(any(feature = "std", test))] use sp_version::NativeVersion; use sp_version::RuntimeVersion; -use xcm::latest::Junction; +use xcm::{ + latest::{InteriorMultiLocation, Junction, Junction::PalletInstance}, + VersionedMultiLocation, +}; +use xcm_builder::PayOverXcm; pub use frame_system::Call as SystemCall; pub use pallet_balances::Call as BalancesCall; @@ -698,6 +707,10 @@ parameter_types! { pub const SpendPeriod: BlockNumber = 6 * DAYS; pub const Burn: Permill = Permill::from_perthousand(2); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); + pub const PayoutSpendPeriod: BlockNumber = 30 * DAYS; + // The asset's interior location for the paying account. This is the Treasury + // pallet instance (which sits at index 37). + pub TreasuryInteriorLocation: InteriorMultiLocation = PalletInstance(37).into(); pub const TipCountdown: BlockNumber = 1 * DAYS; pub const TipFindersFee: Percent = Percent::from_percent(20); @@ -707,6 +720,7 @@ parameter_types! { pub const MaxAuthorities: u32 = 100_000; pub const MaxKeys: u32 = 10_000; pub const MaxPeerInHeartbeats: u32 = 10_000; + pub const MaxBalance: Balance = Balance::max_value(); } impl pallet_treasury::Config for Runtime { @@ -726,6 +740,23 @@ impl pallet_treasury::Config for Runtime { type WeightInfo = weights::pallet_treasury::WeightInfo<Runtime>; type SpendFunds = (); type SpendOrigin = TreasurySpender; + type AssetKind = VersionedLocatableAsset; + type Beneficiary = VersionedMultiLocation; + type BeneficiaryLookup = IdentityLookup<Self::Beneficiary>; + type Paymaster = PayOverXcm< + TreasuryInteriorLocation, + crate::xcm_config::XcmRouter, + crate::XcmPallet, + ConstU32<{ 6 * HOURS }>, + Self::Beneficiary, + Self::AssetKind, + LocatableAssetConverter, + VersionedMultiLocationConverter, + >; + type BalanceConverter = AssetRate; + type PayoutPeriod = PayoutSpendPeriod; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = runtime_common::impls::benchmarks::TreasuryArguments; } impl pallet_offences::Config for Runtime { @@ -1331,6 +1362,18 @@ parameter_types! { pub const MigrationMaxKeyLen: u32 = 512; } +impl pallet_asset_rate::Config for Runtime { + type WeightInfo = weights::pallet_asset_rate::WeightInfo<Runtime>; + type RuntimeEvent = RuntimeEvent; + type CreateOrigin = EnsureRoot<AccountId>; + type RemoveOrigin = EnsureRoot<AccountId>; + type UpdateOrigin = EnsureRoot<AccountId>; + type Currency = Balances; + type AssetKind = <Runtime as pallet_treasury::Config>::AssetKind; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = runtime_common::impls::benchmarks::AssetRateArguments; +} + construct_runtime! { pub enum Runtime { @@ -1443,6 +1486,9 @@ construct_runtime! { // Generalized message queue MessageQueue: pallet_message_queue::{Pallet, Call, Storage, Event<T>} = 100, + + // Asset rate. + AssetRate: pallet_asset_rate::{Pallet, Call, Storage, Event<T>} = 101, } } @@ -1574,6 +1620,7 @@ mod benches { [pallet_utility, Utility] [pallet_vesting, Vesting] [pallet_whitelist, Whitelist] + [pallet_asset_rate, AssetRate] // XCM [pallet_xcm, XcmPallet] // NOTE: Make sure you point to the individual modules below. diff --git a/polkadot/runtime/westend/src/weights/mod.rs b/polkadot/runtime/westend/src/weights/mod.rs index faa94bcac5862..9ae6798d70b6e 100644 --- a/polkadot/runtime/westend/src/weights/mod.rs +++ b/polkadot/runtime/westend/src/weights/mod.rs @@ -17,6 +17,7 @@ pub mod frame_election_provider_support; pub mod frame_system; +pub mod pallet_asset_rate; pub mod pallet_bags_list; pub mod pallet_balances; pub mod pallet_conviction_voting; diff --git a/polkadot/runtime/westend/src/weights/pallet_asset_rate.rs b/polkadot/runtime/westend/src/weights/pallet_asset_rate.rs new file mode 100644 index 0000000000000..810dd01a17026 --- /dev/null +++ b/polkadot/runtime/westend/src/weights/pallet_asset_rate.rs @@ -0,0 +1,86 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see <http://www.gnu.org/licenses/>. + +//! Autogenerated weights for `pallet_asset_rate` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-07-04, STEPS: `50`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `cob`, CPU: `<UNKNOWN>` +//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: Some("polkadot-dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/debug/polkadot +// benchmark +// pallet +// --chain=polkadot-dev +// --steps=50 +// --repeat=2 +// --pallet=pallet_asset_rate +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./runtime/polkadot/src/weights/ +// --header=./file_header.txt + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_asset_rate`. +pub struct WeightInfo<T>(PhantomData<T>); +impl<T: frame_system::Config> pallet_asset_rate::WeightInfo for WeightInfo<T> { + /// Storage: AssetRate ConversionRateToNative (r:1 w:1) + /// Proof: AssetRate ConversionRateToNative (max_values: None, max_size: Some(1237), added: 3712, mode: MaxEncodedLen) + fn create() -> Weight { + // Proof Size summary in bytes: + // Measured: `42` + // Estimated: `4702` + // Minimum execution time: 67_000_000 picoseconds. + Weight::from_parts(69_000_000, 0) + .saturating_add(Weight::from_parts(0, 4702)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: AssetRate ConversionRateToNative (r:1 w:1) + /// Proof: AssetRate ConversionRateToNative (max_values: None, max_size: Some(1237), added: 3712, mode: MaxEncodedLen) + fn update() -> Weight { + // Proof Size summary in bytes: + // Measured: `110` + // Estimated: `4702` + // Minimum execution time: 69_000_000 picoseconds. + Weight::from_parts(71_000_000, 0) + .saturating_add(Weight::from_parts(0, 4702)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: AssetRate ConversionRateToNative (r:1 w:1) + /// Proof: AssetRate ConversionRateToNative (max_values: None, max_size: Some(1237), added: 3712, mode: MaxEncodedLen) + fn remove() -> Weight { + // Proof Size summary in bytes: + // Measured: `110` + // Estimated: `4702` + // Minimum execution time: 70_000_000 picoseconds. + Weight::from_parts(90_000_000, 0) + .saturating_add(Weight::from_parts(0, 4702)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } +} diff --git a/polkadot/runtime/westend/src/weights/pallet_treasury.rs b/polkadot/runtime/westend/src/weights/pallet_treasury.rs index e2eb6abfc7bbb..144e9d5b87238 100644 --- a/polkadot/runtime/westend/src/weights/pallet_treasury.rs +++ b/polkadot/runtime/westend/src/weights/pallet_treasury.rs @@ -17,25 +17,24 @@ //! Autogenerated weights for `pallet_treasury` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-07-13, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-07-07, STEPS: `50`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runner-o7yfgx5n-project-163-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! EXECUTION: ``, WASM-EXECUTION: `Compiled`, CHAIN: `Some("westend-dev")`, DB CACHE: 1024 +//! HOSTNAME: `cob`, CPU: `<UNKNOWN>` +//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: Some("rococo-dev"), DB CACHE: 1024 // Executed Command: -// target/production/polkadot +// ./target/debug/polkadot // benchmark // pallet +// --chain=rococo-dev // --steps=50 -// --repeat=20 +// --repeat=2 +// --pallet=pallet_treasury // --extrinsic=* // --wasm-execution=compiled // --heap-pages=4096 -// --json-file=/builds/parity/mirrors/polkadot/.git/.artifacts/bench.json -// --pallet=pallet_treasury -// --chain=westend-dev +// --output=./runtime/rococo/src/weights/ // --header=./file_header.txt -// --output=./runtime/westend/src/weights/ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -48,103 +47,177 @@ use core::marker::PhantomData; /// Weight functions for `pallet_treasury`. pub struct WeightInfo<T>(PhantomData<T>); impl<T: frame_system::Config> pallet_treasury::WeightInfo for WeightInfo<T> { - /// Storage: `Treasury::ProposalCount` (r:1 w:1) - /// Proof: `Treasury::ProposalCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Treasury::Approvals` (r:1 w:1) - /// Proof: `Treasury::Approvals` (`max_values`: Some(1), `max_size`: Some(402), added: 897, mode: `MaxEncodedLen`) - /// Storage: `Treasury::Proposals` (r:0 w:1) - /// Proof: `Treasury::Proposals` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) - fn spend() -> Weight { + /// Storage: Treasury ProposalCount (r:1 w:1) + /// Proof: Treasury ProposalCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Treasury Approvals (r:1 w:1) + /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// Storage: Treasury Proposals (r:0 w:1) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + fn spend_local() -> Weight { // Proof Size summary in bytes: - // Measured: `6` + // Measured: `42` // Estimated: `1887` - // Minimum execution time: 13_644_000 picoseconds. - Weight::from_parts(13_988_000, 0) + // Minimum execution time: 177_000_000 picoseconds. + Weight::from_parts(191_000_000, 0) .saturating_add(Weight::from_parts(0, 1887)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(3)) } - /// Storage: `Treasury::ProposalCount` (r:1 w:1) - /// Proof: `Treasury::ProposalCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Treasury::Proposals` (r:0 w:1) - /// Proof: `Treasury::Proposals` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: Treasury ProposalCount (r:1 w:1) + /// Proof: Treasury ProposalCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Treasury Proposals (r:0 w:1) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) fn propose_spend() -> Weight { // Proof Size summary in bytes: - // Measured: `107` + // Measured: `143` // Estimated: `1489` - // Minimum execution time: 26_304_000 picoseconds. - Weight::from_parts(26_850_000, 0) + // Minimum execution time: 354_000_000 picoseconds. + Weight::from_parts(376_000_000, 0) .saturating_add(Weight::from_parts(0, 1489)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(2)) } - /// Storage: `Treasury::Proposals` (r:1 w:1) - /// Proof: `Treasury::Proposals` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: Treasury Proposals (r:1 w:1) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) fn reject_proposal() -> Weight { // Proof Size summary in bytes: - // Measured: `265` + // Measured: `301` // Estimated: `3593` - // Minimum execution time: 40_318_000 picoseconds. - Weight::from_parts(41_598_000, 0) + // Minimum execution time: 547_000_000 picoseconds. + Weight::from_parts(550_000_000, 0) .saturating_add(Weight::from_parts(0, 3593)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } - /// Storage: `Treasury::Proposals` (r:1 w:0) - /// Proof: `Treasury::Proposals` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) - /// Storage: `Treasury::Approvals` (r:1 w:1) - /// Proof: `Treasury::Approvals` (`max_values`: Some(1), `max_size`: Some(402), added: 897, mode: `MaxEncodedLen`) + /// Storage: Treasury Proposals (r:1 w:0) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + /// Storage: Treasury Approvals (r:1 w:1) + /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) /// The range of component `p` is `[0, 99]`. fn approve_proposal(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `433 + p * (8 ±0)` + // Measured: `470 + p * (8 ±0)` // Estimated: `3573` - // Minimum execution time: 8_250_000 picoseconds. - Weight::from_parts(10_937_873, 0) + // Minimum execution time: 104_000_000 picoseconds. + Weight::from_parts(121_184_402, 0) .saturating_add(Weight::from_parts(0, 3573)) - // Standard Error: 1_239 - .saturating_add(Weight::from_parts(82_426, 0).saturating_mul(p.into())) + // Standard Error: 42_854 + .saturating_add(Weight::from_parts(153_112, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) } - /// Storage: `Treasury::Approvals` (r:1 w:1) - /// Proof: `Treasury::Approvals` (`max_values`: Some(1), `max_size`: Some(402), added: 897, mode: `MaxEncodedLen`) + /// Storage: Treasury Approvals (r:1 w:1) + /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) fn remove_approval() -> Weight { // Proof Size summary in bytes: - // Measured: `90` + // Measured: `127` // Estimated: `1887` - // Minimum execution time: 6_170_000 picoseconds. - Weight::from_parts(6_366_000, 0) + // Minimum execution time: 80_000_000 picoseconds. + Weight::from_parts(82_000_000, 0) .saturating_add(Weight::from_parts(0, 1887)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } - /// Storage: `Treasury::Deactivated` (r:1 w:1) - /// Proof: `Treasury::Deactivated` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) - /// Storage: `Balances::InactiveIssuance` (r:1 w:1) - /// Proof: `Balances::InactiveIssuance` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) - /// Storage: `Treasury::Approvals` (r:1 w:1) - /// Proof: `Treasury::Approvals` (`max_values`: Some(1), `max_size`: Some(402), added: 897, mode: `MaxEncodedLen`) - /// Storage: `Treasury::Proposals` (r:100 w:100) - /// Proof: `Treasury::Proposals` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:200 w:200) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// The range of component `p` is `[0, 100]`. + /// Storage: Treasury Deactivated (r:1 w:1) + /// Proof: Treasury Deactivated (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: Balances InactiveIssuance (r:1 w:1) + /// Proof: Balances InactiveIssuance (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: Treasury Approvals (r:1 w:1) + /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// Storage: Treasury Proposals (r:99 w:99) + /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + /// Storage: System Account (r:199 w:199) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Bounties BountyApprovals (r:1 w:1) + /// Proof: Bounties BountyApprovals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) + /// The range of component `p` is `[0, 99]`. fn on_initialize_proposals(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `175 + p * (251 ±0)` - // Estimated: `1887 + p * (5206 ±0)` - // Minimum execution time: 39_691_000 picoseconds. - Weight::from_parts(29_703_313, 0) - .saturating_add(Weight::from_parts(0, 1887)) - // Standard Error: 18_540 - .saturating_add(Weight::from_parts(42_601_290, 0).saturating_mul(p.into())) - .saturating_add(T::DbWeight::get().reads(3)) + // Measured: `331 + p * (251 ±0)` + // Estimated: `3593 + p * (5206 ±0)` + // Minimum execution time: 887_000_000 picoseconds. + Weight::from_parts(828_616_021, 0) + .saturating_add(Weight::from_parts(0, 3593)) + // Standard Error: 695_351 + .saturating_add(Weight::from_parts(566_114_524, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(p.into()))) - .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(T::DbWeight::get().writes(5)) .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(p.into()))) .saturating_add(Weight::from_parts(0, 5206).saturating_mul(p.into())) } + /// Storage: AssetRate ConversionRateToNative (r:1 w:0) + /// Proof: AssetRate ConversionRateToNative (max_values: None, max_size: Some(1237), added: 3712, mode: MaxEncodedLen) + /// Storage: Treasury SpendCount (r:1 w:1) + /// Proof: Treasury SpendCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Treasury Spends (r:0 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(1848), added: 4323, mode: MaxEncodedLen) + fn spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `114` + // Estimated: `4702` + // Minimum execution time: 208_000_000 picoseconds. + Weight::from_parts(222_000_000, 0) + .saturating_add(Weight::from_parts(0, 4702)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: Treasury Spends (r:1 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(1848), added: 4323, mode: MaxEncodedLen) + /// Storage: XcmPallet QueryCounter (r:1 w:1) + /// Proof Skipped: XcmPallet QueryCounter (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Configuration ActiveConfig (r:1 w:0) + /// Proof Skipped: Configuration ActiveConfig (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Dmp DeliveryFeeFactor (r:1 w:0) + /// Proof Skipped: Dmp DeliveryFeeFactor (max_values: None, max_size: None, mode: Measured) + /// Storage: XcmPallet SupportedVersion (r:1 w:0) + /// Proof Skipped: XcmPallet SupportedVersion (max_values: None, max_size: None, mode: Measured) + /// Storage: XcmPallet VersionDiscoveryQueue (r:1 w:1) + /// Proof Skipped: XcmPallet VersionDiscoveryQueue (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: XcmPallet SafeXcmVersion (r:1 w:0) + /// Proof Skipped: XcmPallet SafeXcmVersion (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Dmp DownwardMessageQueues (r:1 w:1) + /// Proof Skipped: Dmp DownwardMessageQueues (max_values: None, max_size: None, mode: Measured) + /// Storage: Dmp DownwardMessageQueueHeads (r:1 w:1) + /// Proof Skipped: Dmp DownwardMessageQueueHeads (max_values: None, max_size: None, mode: Measured) + /// Storage: XcmPallet Queries (r:0 w:1) + /// Proof Skipped: XcmPallet Queries (max_values: None, max_size: None, mode: Measured) + fn payout() -> Weight { + // Proof Size summary in bytes: + // Measured: `737` + // Estimated: `5313` + // Minimum execution time: 551_000_000 picoseconds. + Weight::from_parts(569_000_000, 0) + .saturating_add(Weight::from_parts(0, 5313)) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().writes(6)) + } + /// Storage: Treasury Spends (r:1 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(1848), added: 4323, mode: MaxEncodedLen) + /// Storage: XcmPallet Queries (r:1 w:1) + /// Proof Skipped: XcmPallet Queries (max_values: None, max_size: None, mode: Measured) + fn check_status() -> Weight { + // Proof Size summary in bytes: + // Measured: `442` + // Estimated: `5313` + // Minimum execution time: 245_000_000 picoseconds. + Weight::from_parts(281_000_000, 0) + .saturating_add(Weight::from_parts(0, 5313)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: Treasury Spends (r:1 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(1848), added: 4323, mode: MaxEncodedLen) + fn void_spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `172` + // Estimated: `5313` + // Minimum execution time: 147_000_000 picoseconds. + Weight::from_parts(160_000_000, 0) + .saturating_add(Weight::from_parts(0, 5313)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } } diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index f018639b732e3..9e3b8153e2b5c 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -37,7 +37,7 @@ use frame_support::{ parameter_types, traits::{ fungible::{Balanced, Credit, HoldConsideration, ItemOf}, - tokens::{nonfungibles_v2::Inspect, GetSalary, PayFromAccount}, + tokens::{nonfungibles_v2::Inspect, pay::PayAssetFromAccount, GetSalary, PayFromAccount}, AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU16, ConstU32, Contains, Currency, EitherOfDiverse, EqualPrivilegeOnly, Imbalance, InsideBoth, InstanceFilter, KeyOwnerProofSystem, LinearStoragePrice, LockIdentifier, Nothing, OnUnbalanced, @@ -1186,6 +1186,7 @@ parameter_types! { pub const MaximumReasonLength: u32 = 300; pub const MaxApprovals: u32 = 100; pub const MaxBalance: Balance = Balance::max_value(); + pub const SpendPayoutPeriod: BlockNumber = 30 * DAYS; } impl pallet_treasury::Config for Runtime { @@ -1211,6 +1212,14 @@ impl pallet_treasury::Config for Runtime { type WeightInfo = pallet_treasury::weights::SubstrateWeight<Runtime>; type MaxApprovals = MaxApprovals; type SpendOrigin = EnsureWithSuccess<EnsureRoot<AccountId>, AccountId, MaxBalance>; + type AssetKind = u32; + type Beneficiary = AccountId; + type BeneficiaryLookup = Indices; + type Paymaster = PayAssetFromAccount<Assets, TreasuryAccount>; + type BalanceConverter = AssetRate; + type PayoutPeriod = SpendPayoutPeriod; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); } impl pallet_asset_rate::Config for Runtime { diff --git a/substrate/frame/asset-rate/src/lib.rs b/substrate/frame/asset-rate/src/lib.rs index c3dc551f876d0..d4afca8b73c4b 100644 --- a/substrate/frame/asset-rate/src/lib.rs +++ b/substrate/frame/asset-rate/src/lib.rs @@ -240,4 +240,9 @@ where .ok_or(pallet::Error::<T>::UnknownAssetKind.into())?; Ok(rate.saturating_mul_int(balance)) } + /// Set a conversion rate to `1` for the `asset_id`. + #[cfg(feature = "runtime-benchmarks")] + fn ensure_successful(asset_id: AssetKindOf<T>) { + pallet::ConversionRateToNative::<T>::set(asset_id.clone(), Some(1.into())); + } } diff --git a/substrate/frame/bounties/src/tests.rs b/substrate/frame/bounties/src/tests.rs index a6fb89bb86012..4083b05b629cd 100644 --- a/substrate/frame/bounties/src/tests.rs +++ b/substrate/frame/bounties/src/tests.rs @@ -24,7 +24,10 @@ use crate as pallet_bounties; use frame_support::{ assert_noop, assert_ok, parameter_types, - traits::{ConstU32, ConstU64, OnInitialize}, + traits::{ + tokens::{PayFromAccount, UnityAssetBalanceConversion}, + ConstU32, ConstU64, OnInitialize, + }, PalletId, }; @@ -104,6 +107,8 @@ parameter_types! { pub const TreasuryPalletId2: PalletId = PalletId(*b"py/trsr2"); pub static SpendLimit: Balance = u64::MAX; pub static SpendLimit1: Balance = u64::MAX; + pub TreasuryAccount: u128 = Treasury::account_id(); + pub TreasuryInstance1Account: u128 = Treasury1::account_id(); } impl pallet_treasury::Config for Test { @@ -123,6 +128,14 @@ impl pallet_treasury::Config for Test { type SpendFunds = Bounties; type MaxApprovals = ConstU32<100>; type SpendOrigin = frame_system::EnsureRootWithSuccess<Self::AccountId, SpendLimit>; + type AssetKind = (); + type Beneficiary = Self::AccountId; + type BeneficiaryLookup = IdentityLookup<Self::Beneficiary>; + type Paymaster = PayFromAccount<Balances, TreasuryAccount>; + type BalanceConverter = UnityAssetBalanceConversion; + type PayoutPeriod = ConstU64<10>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); } impl pallet_treasury::Config<Instance1> for Test { @@ -142,6 +155,14 @@ impl pallet_treasury::Config<Instance1> for Test { type SpendFunds = Bounties1; type MaxApprovals = ConstU32<100>; type SpendOrigin = frame_system::EnsureRootWithSuccess<Self::AccountId, SpendLimit1>; + type AssetKind = (); + type Beneficiary = Self::AccountId; + type BeneficiaryLookup = IdentityLookup<Self::Beneficiary>; + type Paymaster = PayFromAccount<Balances, TreasuryInstance1Account>; + type BalanceConverter = UnityAssetBalanceConversion; + type PayoutPeriod = ConstU64<10>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); } parameter_types! { diff --git a/substrate/frame/child-bounties/src/tests.rs b/substrate/frame/child-bounties/src/tests.rs index 24a6410f29f78..1fa3d944f3de1 100644 --- a/substrate/frame/child-bounties/src/tests.rs +++ b/substrate/frame/child-bounties/src/tests.rs @@ -24,7 +24,10 @@ use crate as pallet_child_bounties; use frame_support::{ assert_noop, assert_ok, parameter_types, - traits::{ConstU32, ConstU64, OnInitialize}, + traits::{ + tokens::{PayFromAccount, UnityAssetBalanceConversion}, + ConstU32, ConstU64, OnInitialize, + }, weights::Weight, PalletId, }; @@ -104,6 +107,7 @@ parameter_types! { pub const ProposalBond: Permill = Permill::from_percent(5); pub const Burn: Permill = Permill::from_percent(50); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); + pub TreasuryAccount: u128 = Treasury::account_id(); pub const SpendLimit: Balance = u64::MAX; } @@ -124,6 +128,14 @@ impl pallet_treasury::Config for Test { type SpendFunds = Bounties; type MaxApprovals = ConstU32<100>; type SpendOrigin = frame_system::EnsureRootWithSuccess<Self::AccountId, SpendLimit>; + type AssetKind = (); + type Beneficiary = Self::AccountId; + type BeneficiaryLookup = IdentityLookup<Self::Beneficiary>; + type Paymaster = PayFromAccount<Balances, TreasuryAccount>; + type BalanceConverter = UnityAssetBalanceConversion; + type PayoutPeriod = ConstU64<10>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); } parameter_types! { // This will be 50% of the bounty fee. diff --git a/substrate/frame/support/src/traits/tokens.rs b/substrate/frame/support/src/traits/tokens.rs index 253b49c6671f8..3635311e64357 100644 --- a/substrate/frame/support/src/traits/tokens.rs +++ b/substrate/frame/support/src/traits/tokens.rs @@ -31,6 +31,7 @@ pub mod pay; pub use misc::{ AssetId, Balance, BalanceStatus, ConversionFromAssetBalance, ConversionToAssetBalance, ConvertRank, DepositConsequence, ExistenceRequirement, Fortitude, GetSalary, Locker, Precision, - Preservation, Provenance, Restriction, WithdrawConsequence, WithdrawReasons, + Preservation, Provenance, Restriction, UnityAssetBalanceConversion, WithdrawConsequence, + WithdrawReasons, }; pub use pay::{Pay, PayFromAccount, PaymentStatus}; diff --git a/substrate/frame/support/src/traits/tokens/misc.rs b/substrate/frame/support/src/traits/tokens/misc.rs index e8587be101794..fd497bc4eda6f 100644 --- a/substrate/frame/support/src/traits/tokens/misc.rs +++ b/substrate/frame/support/src/traits/tokens/misc.rs @@ -277,6 +277,26 @@ pub trait ConversionFromAssetBalance<AssetBalance, AssetId, OutBalance> { balance: AssetBalance, asset_id: AssetId, ) -> Result<OutBalance, Self::Error>; + /// Ensures that a conversion for the `asset_id` will be successful if done immediately after + /// this call. + #[cfg(feature = "runtime-benchmarks")] + fn ensure_successful(asset_id: AssetId); +} + +/// Implements [`ConversionFromAssetBalance`], enabling a 1:1 conversion of the asset balance +/// value to the balance. +pub struct UnityAssetBalanceConversion; +impl<AssetBalance, AssetId, OutBalance> + ConversionFromAssetBalance<AssetBalance, AssetId, OutBalance> for UnityAssetBalanceConversion +where + AssetBalance: Into<OutBalance>, +{ + type Error = (); + fn from_asset_balance(balance: AssetBalance, _: AssetId) -> Result<OutBalance, Self::Error> { + Ok(balance.into()) + } + #[cfg(feature = "runtime-benchmarks")] + fn ensure_successful(_: AssetId) {} } /// Trait to handle NFT locking mechanism to ensure interactions with the asset can be implemented diff --git a/substrate/frame/support/src/traits/tokens/pay.rs b/substrate/frame/support/src/traits/tokens/pay.rs index 78f8e7b873480..18af7e5e54838 100644 --- a/substrate/frame/support/src/traits/tokens/pay.rs +++ b/substrate/frame/support/src/traits/tokens/pay.rs @@ -23,7 +23,7 @@ use sp_core::{RuntimeDebug, TypedGet}; use sp_runtime::DispatchError; use sp_std::fmt::Debug; -use super::{fungible, Balance, Preservation::Expendable}; +use super::{fungible, fungibles, Balance, Preservation::Expendable}; /// Can be implemented by `PayFromAccount` using a `fungible` impl, but can also be implemented with /// XCM/MultiAsset and made generic over assets. @@ -107,3 +107,36 @@ impl<A: TypedGet, F: fungible::Mutate<A::Type>> Pay for PayFromAccount<F, A> { #[cfg(feature = "runtime-benchmarks")] fn ensure_concluded(_: Self::Id) {} } + +/// Simple implementation of `Pay` for assets which makes a payment from a "pot" - i.e. a single +/// account. +pub struct PayAssetFromAccount<F, A>(sp_std::marker::PhantomData<(F, A)>); +impl<A, F> frame_support::traits::tokens::Pay for PayAssetFromAccount<F, A> +where + A: TypedGet, + F: fungibles::Mutate<A::Type> + fungibles::Create<A::Type>, +{ + type Balance = F::Balance; + type Beneficiary = A::Type; + type AssetKind = F::AssetId; + type Id = (); + type Error = DispatchError; + fn pay( + who: &Self::Beneficiary, + asset: Self::AssetKind, + amount: Self::Balance, + ) -> Result<Self::Id, Self::Error> { + <F as fungibles::Mutate<_>>::transfer(asset, &A::get(), who, amount, Expendable)?; + Ok(()) + } + fn check_payment(_: ()) -> PaymentStatus { + PaymentStatus::Success + } + #[cfg(feature = "runtime-benchmarks")] + fn ensure_successful(_: &Self::Beneficiary, asset: Self::AssetKind, amount: Self::Balance) { + <F as fungibles::Create<_>>::create(asset.clone(), A::get(), true, amount).unwrap(); + <F as fungibles::Mutate<_>>::mint_into(asset, &A::get(), amount).unwrap(); + } + #[cfg(feature = "runtime-benchmarks")] + fn ensure_concluded(_: Self::Id) {} +} diff --git a/substrate/frame/tips/src/tests.rs b/substrate/frame/tips/src/tests.rs index 20d4b2c1a4c11..8fe111afc26a4 100644 --- a/substrate/frame/tips/src/tests.rs +++ b/substrate/frame/tips/src/tests.rs @@ -29,7 +29,10 @@ use sp_storage::Storage; use frame_support::{ assert_noop, assert_ok, parameter_types, storage::StoragePrefixedMap, - traits::{ConstU32, ConstU64, SortedMembers, StorageVersion}, + traits::{ + tokens::{PayFromAccount, UnityAssetBalanceConversion}, + ConstU32, ConstU64, SortedMembers, StorageVersion, + }, PalletId, }; @@ -123,7 +126,10 @@ parameter_types! { pub const Burn: Permill = Permill::from_percent(50); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); pub const TreasuryPalletId2: PalletId = PalletId(*b"py/trsr2"); + pub TreasuryAccount: u128 = Treasury::account_id(); + pub TreasuryInstance1Account: u128 = Treasury1::account_id(); } + impl pallet_treasury::Config for Test { type PalletId = TreasuryPalletId; type Currency = pallet_balances::Pallet<Test>; @@ -141,6 +147,14 @@ impl pallet_treasury::Config for Test { type SpendFunds = (); type MaxApprovals = ConstU32<100>; type SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>; + type AssetKind = (); + type Beneficiary = Self::AccountId; + type BeneficiaryLookup = IdentityLookup<Self::Beneficiary>; + type Paymaster = PayFromAccount<Balances, TreasuryAccount>; + type BalanceConverter = UnityAssetBalanceConversion; + type PayoutPeriod = ConstU64<10>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); } impl pallet_treasury::Config<Instance1> for Test { @@ -160,6 +174,14 @@ impl pallet_treasury::Config<Instance1> for Test { type SpendFunds = (); type MaxApprovals = ConstU32<100>; type SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>; + type AssetKind = (); + type Beneficiary = Self::AccountId; + type BeneficiaryLookup = IdentityLookup<Self::Beneficiary>; + type Paymaster = PayFromAccount<Balances, TreasuryInstance1Account>; + type BalanceConverter = UnityAssetBalanceConversion; + type PayoutPeriod = ConstU64<10>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); } parameter_types! { diff --git a/substrate/frame/treasury/Cargo.toml b/substrate/frame/treasury/Cargo.toml index 785564cd9888d..f7f7a6ae89c56 100644 --- a/substrate/frame/treasury/Cargo.toml +++ b/substrate/frame/treasury/Cargo.toml @@ -17,6 +17,7 @@ codec = { package = "parity-scale-codec", version = "3.6.1", default-features = "derive", "max-encoded-len", ] } +docify = "0.2.0" impl-trait-for-tuples = "0.2.2" scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } serde = { version = "1.0.188", features = ["derive"], optional = true } @@ -26,11 +27,12 @@ frame-system = { path = "../system", default-features = false} pallet-balances = { path = "../balances", default-features = false} sp-runtime = { path = "../../primitives/runtime", default-features = false} sp-std = { path = "../../primitives/std", default-features = false} +sp-core = { path = "../../primitives/core", default-features = false, optional = true} [dev-dependencies] -sp-core = { path = "../../primitives/core" } sp-io = { path = "../../primitives/io" } pallet-utility = { path = "../utility" } +sp-core = { path = "../../primitives/core", default-features = false } [features] default = [ "std" ] @@ -43,12 +45,13 @@ std = [ "pallet-utility/std", "scale-info/std", "serde", - "sp-core/std", + "sp-core?/std", "sp-io/std", "sp-runtime/std", "sp-std/std", ] runtime-benchmarks = [ + "dep:sp-core", "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", diff --git a/substrate/frame/treasury/src/benchmarking.rs b/substrate/frame/treasury/src/benchmarking.rs index 24c290ddb665e..f5f73ea8ddabd 100644 --- a/substrate/frame/treasury/src/benchmarking.rs +++ b/substrate/frame/treasury/src/benchmarking.rs @@ -21,12 +21,41 @@ use super::{Pallet as Treasury, *}; -use frame_benchmarking::v1::{account, benchmarks_instance_pallet, BenchmarkError}; +use frame_benchmarking::{ + v1::{account, BenchmarkError}, + v2::*, +}; use frame_support::{ ensure, - traits::{EnsureOrigin, OnInitialize, UnfilteredDispatchable}, + traits::{ + tokens::{ConversionFromAssetBalance, PaymentStatus}, + EnsureOrigin, OnInitialize, + }, }; use frame_system::RawOrigin; +use sp_core::crypto::FromEntropy; + +/// Trait describing factory functions for dispatchables' parameters. +pub trait ArgumentsFactory<AssetKind, Beneficiary> { + /// Factory function for an asset kind. + fn create_asset_kind(seed: u32) -> AssetKind; + /// Factory function for a beneficiary. + fn create_beneficiary(seed: [u8; 32]) -> Beneficiary; +} + +/// Implementation that expects the parameters implement the [`FromEntropy`] trait. +impl<AssetKind, Beneficiary> ArgumentsFactory<AssetKind, Beneficiary> for () +where + AssetKind: FromEntropy, + Beneficiary: FromEntropy, +{ + fn create_asset_kind(seed: u32) -> AssetKind { + AssetKind::from_entropy(&mut seed.encode().as_slice()).unwrap() + } + fn create_beneficiary(seed: [u8; 32]) -> Beneficiary { + Beneficiary::from_entropy(&mut seed.as_slice()).unwrap() + } +} const SEED: u32 = 0; @@ -66,81 +95,245 @@ fn assert_last_event<T: Config<I>, I: 'static>(generic_event: <T as Config<I>>:: frame_system::Pallet::<T>::assert_last_event(generic_event.into()); } -benchmarks_instance_pallet! { +// Create the arguments for the `spend` dispatchable. +fn create_spend_arguments<T: Config<I>, I: 'static>( + seed: u32, +) -> (T::AssetKind, AssetBalanceOf<T, I>, T::Beneficiary, BeneficiaryLookupOf<T, I>) { + let asset_kind = T::BenchmarkHelper::create_asset_kind(seed); + let beneficiary = T::BenchmarkHelper::create_beneficiary([seed.try_into().unwrap(); 32]); + let beneficiary_lookup = T::BeneficiaryLookup::unlookup(beneficiary.clone()); + (asset_kind, 100u32.into(), beneficiary, beneficiary_lookup) +} + +#[instance_benchmarks] +mod benchmarks { + use super::*; + // This benchmark is short-circuited if `SpendOrigin` cannot provide // a successful origin, in which case `spend` is un-callable and can use weight=0. - spend { + #[benchmark] + fn spend_local() -> Result<(), BenchmarkError> { let (_, value, beneficiary_lookup) = setup_proposal::<T, _>(SEED); - let origin = T::SpendOrigin::try_successful_origin(); + let origin = + T::SpendOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; let beneficiary = T::Lookup::lookup(beneficiary_lookup.clone()).unwrap(); - let call = Call::<T, I>::spend { amount: value, beneficiary: beneficiary_lookup }; - }: { - if let Ok(origin) = origin.clone() { - call.dispatch_bypass_filter(origin)?; - } - } - verify { - if origin.is_ok() { - assert_last_event::<T, I>(Event::SpendApproved { proposal_index: 0, amount: value, beneficiary }.into()) - } + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, value, beneficiary_lookup); + + assert_last_event::<T, I>( + Event::SpendApproved { proposal_index: 0, amount: value, beneficiary }.into(), + ); + Ok(()) } - propose_spend { + #[benchmark] + fn propose_spend() -> Result<(), BenchmarkError> { let (caller, value, beneficiary_lookup) = setup_proposal::<T, _>(SEED); // Whitelist caller account from further DB operations. let caller_key = frame_system::Account::<T>::hashed_key_for(&caller); frame_benchmarking::benchmarking::add_to_whitelist(caller_key.into()); - }: _(RawOrigin::Signed(caller), value, beneficiary_lookup) - reject_proposal { + #[extrinsic_call] + _(RawOrigin::Signed(caller), value, beneficiary_lookup); + + Ok(()) + } + + #[benchmark] + fn reject_proposal() -> Result<(), BenchmarkError> { let (caller, value, beneficiary_lookup) = setup_proposal::<T, _>(SEED); #[allow(deprecated)] Treasury::<T, _>::propose_spend( RawOrigin::Signed(caller).into(), value, - beneficiary_lookup + beneficiary_lookup, )?; let proposal_id = Treasury::<T, _>::proposal_count() - 1; let reject_origin = T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; - }: _<T::RuntimeOrigin>(reject_origin, proposal_id) - approve_proposal { - let p in 0 .. T::MaxApprovals::get() - 1; + #[extrinsic_call] + _(reject_origin as T::RuntimeOrigin, proposal_id); + + Ok(()) + } + + #[benchmark] + fn approve_proposal( + p: Linear<0, { T::MaxApprovals::get() - 1 }>, + ) -> Result<(), BenchmarkError> { create_approved_proposals::<T, _>(p)?; let (caller, value, beneficiary_lookup) = setup_proposal::<T, _>(SEED); #[allow(deprecated)] Treasury::<T, _>::propose_spend( RawOrigin::Signed(caller).into(), value, - beneficiary_lookup + beneficiary_lookup, )?; let proposal_id = Treasury::<T, _>::proposal_count() - 1; let approve_origin = T::ApproveOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; - }: _<T::RuntimeOrigin>(approve_origin, proposal_id) - remove_approval { + #[extrinsic_call] + _(approve_origin as T::RuntimeOrigin, proposal_id); + + Ok(()) + } + + #[benchmark] + fn remove_approval() -> Result<(), BenchmarkError> { let (caller, value, beneficiary_lookup) = setup_proposal::<T, _>(SEED); #[allow(deprecated)] Treasury::<T, _>::propose_spend( RawOrigin::Signed(caller).into(), value, - beneficiary_lookup + beneficiary_lookup, )?; let proposal_id = Treasury::<T, _>::proposal_count() - 1; #[allow(deprecated)] Treasury::<T, I>::approve_proposal(RawOrigin::Root.into(), proposal_id)?; let reject_origin = T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; - }: _<T::RuntimeOrigin>(reject_origin, proposal_id) - on_initialize_proposals { - let p in 0 .. T::MaxApprovals::get(); + #[extrinsic_call] + _(reject_origin as T::RuntimeOrigin, proposal_id); + + Ok(()) + } + + #[benchmark] + fn on_initialize_proposals( + p: Linear<0, { T::MaxApprovals::get() - 1 }>, + ) -> Result<(), BenchmarkError> { setup_pot_account::<T, _>(); create_approved_proposals::<T, _>(p)?; - }: { - Treasury::<T, _>::on_initialize(frame_system::pallet_prelude::BlockNumberFor::<T>::zero()); + + #[block] + { + Treasury::<T, _>::on_initialize(0u32.into()); + } + + Ok(()) + } + + #[benchmark] + fn spend() -> Result<(), BenchmarkError> { + let origin = + T::SpendOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + let (asset_kind, amount, beneficiary, beneficiary_lookup) = + create_spend_arguments::<T, _>(SEED); + T::BalanceConverter::ensure_successful(asset_kind.clone()); + + #[extrinsic_call] + _( + origin as T::RuntimeOrigin, + Box::new(asset_kind.clone()), + amount, + Box::new(beneficiary_lookup), + None, + ); + + let valid_from = frame_system::Pallet::<T>::block_number(); + let expire_at = valid_from.saturating_add(T::PayoutPeriod::get()); + assert_last_event::<T, I>( + Event::AssetSpendApproved { + index: 0, + asset_kind, + amount, + beneficiary, + valid_from, + expire_at, + } + .into(), + ); + Ok(()) + } + + #[benchmark] + fn payout() -> Result<(), BenchmarkError> { + let origin = T::SpendOrigin::try_successful_origin().map_err(|_| "No origin")?; + let (asset_kind, amount, beneficiary, beneficiary_lookup) = + create_spend_arguments::<T, _>(SEED); + T::BalanceConverter::ensure_successful(asset_kind.clone()); + Treasury::<T, _>::spend( + origin, + Box::new(asset_kind.clone()), + amount, + Box::new(beneficiary_lookup), + None, + )?; + T::Paymaster::ensure_successful(&beneficiary, asset_kind, amount); + let caller: T::AccountId = account("caller", 0, SEED); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), 0u32); + + let id = match Spends::<T, I>::get(0).unwrap().status { + PaymentState::Attempted { id, .. } => { + assert_ne!(T::Paymaster::check_payment(id), PaymentStatus::Failure); + id + }, + _ => panic!("No payout attempt made"), + }; + assert_last_event::<T, I>(Event::Paid { index: 0, payment_id: id }.into()); + assert!(Treasury::<T, _>::payout(RawOrigin::Signed(caller).into(), 0u32).is_err()); + Ok(()) + } + + #[benchmark] + fn check_status() -> Result<(), BenchmarkError> { + let origin = T::SpendOrigin::try_successful_origin().map_err(|_| "No origin")?; + let (asset_kind, amount, beneficiary, beneficiary_lookup) = + create_spend_arguments::<T, _>(SEED); + T::BalanceConverter::ensure_successful(asset_kind.clone()); + Treasury::<T, _>::spend( + origin, + Box::new(asset_kind.clone()), + amount, + Box::new(beneficiary_lookup), + None, + )?; + T::Paymaster::ensure_successful(&beneficiary, asset_kind, amount); + let caller: T::AccountId = account("caller", 0, SEED); + Treasury::<T, _>::payout(RawOrigin::Signed(caller.clone()).into(), 0u32)?; + match Spends::<T, I>::get(0).unwrap().status { + PaymentState::Attempted { id, .. } => { + T::Paymaster::ensure_concluded(id); + }, + _ => panic!("No payout attempt made"), + }; + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), 0u32); + + if let Some(s) = Spends::<T, I>::get(0) { + assert!(!matches!(s.status, PaymentState::Attempted { .. })); + } + Ok(()) + } + + #[benchmark] + fn void_spend() -> Result<(), BenchmarkError> { + let origin = T::SpendOrigin::try_successful_origin().map_err(|_| "No origin")?; + let (asset_kind, amount, _, beneficiary_lookup) = create_spend_arguments::<T, _>(SEED); + T::BalanceConverter::ensure_successful(asset_kind.clone()); + Treasury::<T, _>::spend( + origin, + Box::new(asset_kind.clone()), + amount, + Box::new(beneficiary_lookup), + None, + )?; + assert!(Spends::<T, I>::get(0).is_some()); + let origin = + T::RejectOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, 0u32); + + assert!(Spends::<T, I>::get(0).is_none()); + Ok(()) } impl_benchmark_test_suite!(Treasury, crate::tests::new_test_ext(), crate::tests::Test); diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index 730fae2a4e92c..b2b3a8801c156 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -15,46 +15,60 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! > Made with *Substrate*, for *Polkadot*. +//! +//! [![github]](https://github.com/paritytech/substrate/frame/fast-unstake) - +//! [![polkadot]](https://polkadot.network) +//! +//! [polkadot]: https://img.shields.io/badge/polkadot-E6007A?style=for-the-badge&logo=polkadot&logoColor=white +//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github +//! //! # Treasury Pallet //! //! The Treasury pallet provides a "pot" of funds that can be managed by stakeholders in the system //! and a structure for making spending proposals from this pot. //! -//! - [`Config`] -//! - [`Call`] -//! //! ## Overview //! //! The Treasury Pallet itself provides the pot to store funds, and a means for stakeholders to -//! propose, approve, and deny expenditures. The chain will need to provide a method (e.g. -//! inflation, fees) for collecting funds. +//! propose and claim expenditures (aka spends). The chain will need to provide a method to approve +//! spends (e.g. public referendum) and a method for collecting funds (e.g. inflation, fees). //! -//! By way of example, the Council could vote to fund the Treasury with a portion of the block +//! By way of example, stakeholders could vote to fund the Treasury with a portion of the block //! reward and use the funds to pay developers. //! -//! //! ### Terminology //! //! - **Proposal:** A suggestion to allocate funds from the pot to a beneficiary. //! - **Beneficiary:** An account who will receive the funds from a proposal iff the proposal is //! approved. -//! - **Deposit:** Funds that a proposer must lock when making a proposal. The deposit will be -//! returned or slashed if the proposal is approved or rejected respectively. //! - **Pot:** Unspent funds accumulated by the treasury pallet. +//! - **Spend** An approved proposal for transferring a specific amount of funds to a designated +//! beneficiary. //! -//! ## Interface +//! ### Example //! -//! ### Dispatchable Functions +//! 1. Multiple local spends approved by spend origins and received by a beneficiary. +#![doc = docify::embed!("src/tests.rs", spend_local_origin_works)] //! -//! General spending/proposal protocol: -//! - `propose_spend` - Make a spending proposal and stake the required deposit. -//! - `reject_proposal` - Reject a proposal, slashing the deposit. -//! - `approve_proposal` - Accept the proposal, returning the deposit. -//! - `remove_approval` - Remove an approval, the deposit will no longer be returned. +//! 2. Approve a spend of some asset kind and claim it. +#![doc = docify::embed!("src/tests.rs", spend_payout_works)] //! -//! ## GenesisConfig +//! ## Pallet API //! -//! The Treasury pallet depends on the [`GenesisConfig`]. +//! See the [`pallet`] module for more information about the interfaces this pallet exposes, +//! including its configuration trait, dispatchables, storage items, events and errors. +//! +//! ## Low Level / Implementation Details +//! +//! Spends can be initiated using either the `spend_local` or `spend` dispatchable. The +//! `spend_local` dispatchable enables the creation of spends using the native currency of the +//! chain, utilizing the funds stored in the pot. These spends are automatically paid out every +//! [`pallet::Config::SpendPeriod`]. On the other hand, the `spend` dispatchable allows spending of +//! any asset kind managed by the treasury, with payment facilitated by a designated +//! [`pallet::Config::Paymaster`]. To claim these spends, the `payout` dispatchable should be called +//! within some temporal bounds, starting from the moment they become valid and within one +//! [`pallet::Config::PayoutPeriod`]. #![cfg_attr(not(feature = "std"), no_std)] @@ -62,6 +76,8 @@ mod benchmarking; #[cfg(test)] mod tests; pub mod weights; +#[cfg(feature = "runtime-benchmarks")] +pub use benchmarking::ArgumentsFactory; use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -75,7 +91,7 @@ use sp_std::{collections::btree_map::BTreeMap, prelude::*}; use frame_support::{ print, traits::{ - Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced, + tokens::Pay, Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced, ReservableCurrency, WithdrawReasons, }, weights::Weight, @@ -87,6 +103,7 @@ pub use weights::WeightInfo; pub type BalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance; +pub type AssetBalanceOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Balance; pub type PositiveImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currency< <T as frame_system::Config>::AccountId, >>::PositiveImbalance; @@ -94,6 +111,7 @@ pub type NegativeImbalanceOf<T, I = ()> = <<T as Config<I>>::Currency as Currenc <T as frame_system::Config>::AccountId, >>::NegativeImbalance; type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source; +type BeneficiaryLookupOf<T, I> = <<T as Config<I>>::BeneficiaryLookup as StaticLookup>::Source; /// A trait to allow the Treasury Pallet to spend it's funds for other purposes. /// There is an expectation that the implementer of this trait will correctly manage @@ -133,10 +151,47 @@ pub struct Proposal<AccountId, Balance> { bond: Balance, } +/// The state of the payment claim. +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)] +pub enum PaymentState<Id> { + /// Pending claim. + Pending, + /// Payment attempted with a payment identifier. + Attempted { id: Id }, + /// Payment failed. + Failed, +} + +/// Info regarding an approved treasury spend. +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)] +pub struct SpendStatus<AssetKind, AssetBalance, Beneficiary, BlockNumber, PaymentId> { + // The kind of asset to be spent. + asset_kind: AssetKind, + /// The asset amount of the spend. + amount: AssetBalance, + /// The beneficiary of the spend. + beneficiary: Beneficiary, + /// The block number from which the spend can be claimed. + valid_from: BlockNumber, + /// The block number by which the spend has to be claimed. + expire_at: BlockNumber, + /// The status of the payout/claim. + status: PaymentState<PaymentId>, +} + +/// Index of an approved treasury spend. +pub type SpendIndex = u32; + #[frame_support::pallet] pub mod pallet { use super::*; - use frame_support::{dispatch_context::with_context, pallet_prelude::*}; + use frame_support::{ + dispatch_context::with_context, + pallet_prelude::*, + traits::tokens::{ConversionFromAssetBalance, PaymentStatus}, + }; use frame_system::pallet_prelude::*; #[pallet::pallet] @@ -201,9 +256,38 @@ pub mod pallet { type MaxApprovals: Get<u32>; /// The origin required for approving spends from the treasury outside of the proposal - /// process. The `Success` value is the maximum amount that this origin is allowed to - /// spend at a time. + /// process. The `Success` value is the maximum amount in a native asset that this origin + /// is allowed to spend at a time. type SpendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BalanceOf<Self, I>>; + + /// Type parameter representing the asset kinds to be spent from the treasury. + type AssetKind: Parameter + MaxEncodedLen; + + /// Type parameter used to identify the beneficiaries eligible to receive treasury spends. + type Beneficiary: Parameter + MaxEncodedLen; + + /// Converting trait to take a source type and convert to [`Self::Beneficiary`]. + type BeneficiaryLookup: StaticLookup<Target = Self::Beneficiary>; + + /// Type for processing spends of [Self::AssetKind] in favor of [`Self::Beneficiary`]. + type Paymaster: Pay<Beneficiary = Self::Beneficiary, AssetKind = Self::AssetKind>; + + /// Type for converting the balance of an [Self::AssetKind] to the balance of the native + /// asset, solely for the purpose of asserting the result against the maximum allowed spend + /// amount of the [`Self::SpendOrigin`]. + type BalanceConverter: ConversionFromAssetBalance< + <Self::Paymaster as Pay>::Balance, + Self::AssetKind, + BalanceOf<Self, I>, + >; + + /// The period during which an approved treasury spend has to be claimed. + #[pallet::constant] + type PayoutPeriod: Get<BlockNumberFor<Self>>; + + /// Helper type for benchmarks. + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: ArgumentsFactory<Self::AssetKind, Self::Beneficiary>; } /// Number of proposals that have been made. @@ -233,6 +317,27 @@ pub mod pallet { pub type Approvals<T: Config<I>, I: 'static = ()> = StorageValue<_, BoundedVec<ProposalIndex, T::MaxApprovals>, ValueQuery>; + /// The count of spends that have been made. + #[pallet::storage] + pub(crate) type SpendCount<T, I = ()> = StorageValue<_, SpendIndex, ValueQuery>; + + /// Spends that have been approved and being processed. + // Hasher: Twox safe since `SpendIndex` is an internal count based index. + #[pallet::storage] + pub type Spends<T: Config<I>, I: 'static = ()> = StorageMap< + _, + Twox64Concat, + SpendIndex, + SpendStatus< + T::AssetKind, + AssetBalanceOf<T, I>, + T::Beneficiary, + BlockNumberFor<T>, + <T::Paymaster as Pay>::Id, + >, + OptionQuery, + >; + #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig<T: Config<I>, I: 'static = ()> { @@ -277,6 +382,24 @@ pub mod pallet { }, /// The inactive funds of the pallet have been updated. UpdatedInactive { reactivated: BalanceOf<T, I>, deactivated: BalanceOf<T, I> }, + /// A new asset spend proposal has been approved. + AssetSpendApproved { + index: SpendIndex, + asset_kind: T::AssetKind, + amount: AssetBalanceOf<T, I>, + beneficiary: T::Beneficiary, + valid_from: BlockNumberFor<T>, + expire_at: BlockNumberFor<T>, + }, + /// An approved spend was voided. + AssetSpendVoided { index: SpendIndex }, + /// A payment happened. + Paid { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id }, + /// A payment failed and can be retried. + PaymentFailed { index: SpendIndex, payment_id: <T::Paymaster as Pay>::Id }, + /// A spend was processed and removed from the storage. It might have been successfully + /// paid or it may have expired. + SpendProcessed { index: SpendIndex }, } /// Error for the treasury pallet. @@ -284,7 +407,7 @@ pub mod pallet { pub enum Error<T, I = ()> { /// Proposer's balance is too low. InsufficientProposersBalance, - /// No proposal or bounty at that index. + /// No proposal, bounty or spend at that index. InvalidIndex, /// Too many approvals in the queue. TooManyApprovals, @@ -293,6 +416,20 @@ pub mod pallet { InsufficientPermission, /// Proposal has not been approved. ProposalNotApproved, + /// The balance of the asset kind is not convertible to the balance of the native asset. + FailedToConvertBalance, + /// The spend has expired and cannot be claimed. + SpendExpired, + /// The spend is not yet eligible for payout. + EarlyPayout, + /// The payment has already been attempted. + AlreadyAttempted, + /// There was some issue with the mechanism of payment. + PayoutError, + /// The payout was not yet attempted/claimed. + NotAttempted, + /// The payment has neither failed nor succeeded yet. + Inconclusive, } #[pallet::hooks] @@ -328,12 +465,22 @@ pub mod pallet { #[pallet::call] impl<T: Config<I>, I: 'static> Pallet<T, I> { - /// Put forward a suggestion for spending. A deposit proportional to the value - /// is reserved and slashed if the proposal is rejected. It is returned once the - /// proposal is awarded. + /// Put forward a suggestion for spending. /// - /// ## Complexity + /// ## Dispatch Origin + /// + /// Must be signed. + /// + /// ## Details + /// A deposit proportional to the value is reserved and slashed if the proposal is rejected. + /// It is returned once the proposal is awarded. + /// + /// ### Complexity /// - O(1) + /// + /// ## Events + /// + /// Emits [`Event::Proposed`] if successful. #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::propose_spend())] #[allow(deprecated)] @@ -360,12 +507,21 @@ pub mod pallet { Ok(()) } - /// Reject a proposed spend. The original deposit will be slashed. + /// Reject a proposed spend. /// - /// May only be called from `T::RejectOrigin`. + /// ## Dispatch Origin /// - /// ## Complexity + /// Must be [`Config::RejectOrigin`]. + /// + /// ## Details + /// The original deposit will be slashed. + /// + /// ### Complexity /// - O(1) + /// + /// ## Events + /// + /// Emits [`Event::Rejected`] if successful. #[pallet::call_index(1)] #[pallet::weight((T::WeightInfo::reject_proposal(), DispatchClass::Operational))] #[allow(deprecated)] @@ -391,13 +547,23 @@ pub mod pallet { Ok(()) } - /// Approve a proposal. At a later time, the proposal will be allocated to the beneficiary - /// and the original deposit will be returned. + /// Approve a proposal. /// - /// May only be called from `T::ApproveOrigin`. + /// ## Dispatch Origin /// - /// ## Complexity + /// Must be [`Config::ApproveOrigin`]. + /// + /// ## Details + /// + /// At a later time, the proposal will be allocated to the beneficiary and the original + /// deposit will be returned. + /// + /// ### Complexity /// - O(1). + /// + /// ## Events + /// + /// No events are emitted from this dispatch. #[pallet::call_index(2)] #[pallet::weight((T::WeightInfo::approve_proposal(T::MaxApprovals::get()), DispatchClass::Operational))] #[allow(deprecated)] @@ -418,15 +584,24 @@ pub mod pallet { /// Propose and approve a spend of treasury funds. /// - /// - `origin`: Must be `SpendOrigin` with the `Success` value being at least `amount`. - /// - `amount`: The amount to be transferred from the treasury to the `beneficiary`. - /// - `beneficiary`: The destination account for the transfer. + /// ## Dispatch Origin /// + /// Must be [`Config::SpendOrigin`] with the `Success` value being at least `amount`. + /// + /// ### Details /// NOTE: For record-keeping purposes, the proposer is deemed to be equivalent to the /// beneficiary. + /// + /// ### Parameters + /// - `amount`: The amount to be transferred from the treasury to the `beneficiary`. + /// - `beneficiary`: The destination account for the transfer. + /// + /// ## Events + /// + /// Emits [`Event::SpendApproved`] if successful. #[pallet::call_index(3)] - #[pallet::weight(T::WeightInfo::spend())] - pub fn spend( + #[pallet::weight(T::WeightInfo::spend_local())] + pub fn spend_local( origin: OriginFor<T>, #[pallet::compact] amount: BalanceOf<T, I>, beneficiary: AccountIdLookupOf<T>, @@ -472,18 +647,26 @@ pub mod pallet { } /// Force a previously approved proposal to be removed from the approval queue. + /// + /// ## Dispatch Origin + /// + /// Must be [`Config::RejectOrigin`]. + /// + /// ## Details + /// /// The original deposit will no longer be returned. /// - /// May only be called from `T::RejectOrigin`. + /// ### Parameters /// - `proposal_id`: The index of a proposal /// - /// ## Complexity + /// ### Complexity /// - O(A) where `A` is the number of approvals /// - /// Errors: - /// - `ProposalNotApproved`: The `proposal_id` supplied was not found in the approval queue, - /// i.e., the proposal has not been approved. This could also mean the proposal does not - /// exist altogether, thus there is no way it would have been approved in the first place. + /// ### Errors + /// - [`Error::ProposalNotApproved`]: The `proposal_id` supplied was not found in the + /// approval queue, i.e., the proposal has not been approved. This could also mean the + /// proposal does not exist altogether, thus there is no way it would have been approved + /// in the first place. #[pallet::call_index(4)] #[pallet::weight((T::WeightInfo::remove_approval(), DispatchClass::Operational))] pub fn remove_approval( @@ -503,6 +686,229 @@ pub mod pallet { Ok(()) } + + /// Propose and approve a spend of treasury funds. + /// + /// ## Dispatch Origin + /// + /// Must be [`Config::SpendOrigin`] with the `Success` value being at least + /// `amount` of `asset_kind` in the native asset. The amount of `asset_kind` is converted + /// for assertion using the [`Config::BalanceConverter`]. + /// + /// ## Details + /// + /// Create an approved spend for transferring a specific `amount` of `asset_kind` to a + /// designated beneficiary. The spend must be claimed using the `payout` dispatchable within + /// the [`Config::PayoutPeriod`]. + /// + /// ### Parameters + /// - `asset_kind`: An indicator of the specific asset class to be spent. + /// - `amount`: The amount to be transferred from the treasury to the `beneficiary`. + /// - `beneficiary`: The beneficiary of the spend. + /// - `valid_from`: The block number from which the spend can be claimed. It can refer to + /// the past if the resulting spend has not yet expired according to the + /// [`Config::PayoutPeriod`]. If `None`, the spend can be claimed immediately after + /// approval. + /// + /// ## Events + /// + /// Emits [`Event::AssetSpendApproved`] if successful. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::spend())] + pub fn spend( + origin: OriginFor<T>, + asset_kind: Box<T::AssetKind>, + #[pallet::compact] amount: AssetBalanceOf<T, I>, + beneficiary: Box<BeneficiaryLookupOf<T, I>>, + valid_from: Option<BlockNumberFor<T>>, + ) -> DispatchResult { + let max_amount = T::SpendOrigin::ensure_origin(origin)?; + let beneficiary = T::BeneficiaryLookup::lookup(*beneficiary)?; + + let now = frame_system::Pallet::<T>::block_number(); + let valid_from = valid_from.unwrap_or(now); + let expire_at = valid_from.saturating_add(T::PayoutPeriod::get()); + ensure!(expire_at > now, Error::<T, I>::SpendExpired); + + let native_amount = + T::BalanceConverter::from_asset_balance(amount, *asset_kind.clone()) + .map_err(|_| Error::<T, I>::FailedToConvertBalance)?; + + ensure!(native_amount <= max_amount, Error::<T, I>::InsufficientPermission); + + with_context::<SpendContext<BalanceOf<T, I>>, _>(|v| { + let context = v.or_default(); + // We group based on `max_amount`, to distinguish between different kind of + // origins. (assumes that all origins have different `max_amount`) + // + // Worst case is that we reject some "valid" request. + let spend = context.spend_in_context.entry(max_amount).or_default(); + + // Ensure that we don't overflow nor use more than `max_amount` + if spend.checked_add(&native_amount).map(|s| s > max_amount).unwrap_or(true) { + Err(Error::<T, I>::InsufficientPermission) + } else { + *spend = spend.saturating_add(native_amount); + Ok(()) + } + }) + .unwrap_or(Ok(()))?; + + let index = SpendCount::<T, I>::get(); + Spends::<T, I>::insert( + index, + SpendStatus { + asset_kind: *asset_kind.clone(), + amount, + beneficiary: beneficiary.clone(), + valid_from, + expire_at, + status: PaymentState::Pending, + }, + ); + SpendCount::<T, I>::put(index + 1); + + Self::deposit_event(Event::AssetSpendApproved { + index, + asset_kind: *asset_kind, + amount, + beneficiary, + valid_from, + expire_at, + }); + Ok(()) + } + + /// Claim a spend. + /// + /// ## Dispatch Origin + /// + /// Must be signed. + /// + /// ## Details + /// + /// Spends must be claimed within some temporal bounds. A spend may be claimed within one + /// [`Config::PayoutPeriod`] from the `valid_from` block. + /// In case of a payout failure, the spend status must be updated with the `check_status` + /// dispatchable before retrying with the current function. + /// + /// ### Parameters + /// - `index`: The spend index. + /// + /// ## Events + /// + /// Emits [`Event::Paid`] if successful. + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::payout())] + pub fn payout(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult { + ensure_signed(origin)?; + let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?; + let now = frame_system::Pallet::<T>::block_number(); + ensure!(now >= spend.valid_from, Error::<T, I>::EarlyPayout); + ensure!(spend.expire_at > now, Error::<T, I>::SpendExpired); + ensure!( + matches!(spend.status, PaymentState::Pending | PaymentState::Failed), + Error::<T, I>::AlreadyAttempted + ); + + let id = T::Paymaster::pay(&spend.beneficiary, spend.asset_kind.clone(), spend.amount) + .map_err(|_| Error::<T, I>::PayoutError)?; + + spend.status = PaymentState::Attempted { id }; + Spends::<T, I>::insert(index, spend); + + Self::deposit_event(Event::<T, I>::Paid { index, payment_id: id }); + + Ok(()) + } + + /// Check the status of the spend and remove it from the storage if processed. + /// + /// ## Dispatch Origin + /// + /// Must be signed. + /// + /// ## Details + /// + /// The status check is a prerequisite for retrying a failed payout. + /// If a spend has either succeeded or expired, it is removed from the storage by this + /// function. In such instances, transaction fees are refunded. + /// + /// ### Parameters + /// - `index`: The spend index. + /// + /// ## Events + /// + /// Emits [`Event::PaymentFailed`] if the spend payout has failed. + /// Emits [`Event::SpendProcessed`] if the spend payout has succeed. + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::check_status())] + pub fn check_status(origin: OriginFor<T>, index: SpendIndex) -> DispatchResultWithPostInfo { + use PaymentState as State; + use PaymentStatus as Status; + + ensure_signed(origin)?; + let mut spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?; + let now = frame_system::Pallet::<T>::block_number(); + + if now > spend.expire_at && !matches!(spend.status, State::Attempted { .. }) { + // spend has expired and no further status update is expected. + Spends::<T, I>::remove(index); + Self::deposit_event(Event::<T, I>::SpendProcessed { index }); + return Ok(Pays::No.into()) + } + + let payment_id = match spend.status { + State::Attempted { id } => id, + _ => return Err(Error::<T, I>::NotAttempted.into()), + }; + + match T::Paymaster::check_payment(payment_id) { + Status::Failure => { + spend.status = PaymentState::Failed; + Spends::<T, I>::insert(index, spend); + Self::deposit_event(Event::<T, I>::PaymentFailed { index, payment_id }); + }, + Status::Success | Status::Unknown => { + Spends::<T, I>::remove(index); + Self::deposit_event(Event::<T, I>::SpendProcessed { index }); + return Ok(Pays::No.into()) + }, + Status::InProgress => return Err(Error::<T, I>::Inconclusive.into()), + } + return Ok(Pays::Yes.into()) + } + + /// Void previously approved spend. + /// + /// ## Dispatch Origin + /// + /// Must be [`Config::RejectOrigin`]. + /// + /// ## Details + /// + /// A spend void is only possible if the payout has not been attempted yet. + /// + /// ### Parameters + /// - `index`: The spend index. + /// + /// ## Events + /// + /// Emits [`Event::AssetSpendVoided`] if successful. + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::void_spend())] + pub fn void_spend(origin: OriginFor<T>, index: SpendIndex) -> DispatchResult { + T::RejectOrigin::ensure_origin(origin)?; + let spend = Spends::<T, I>::get(index).ok_or(Error::<T, I>::InvalidIndex)?; + ensure!( + matches!(spend.status, PaymentState::Pending | PaymentState::Failed), + Error::<T, I>::AlreadyAttempted + ); + + Spends::<T, I>::remove(index); + Self::deposit_event(Event::<T, I>::AssetSpendVoided { index }); + Ok(()) + } } } diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index ba45d5f6ff16f..4bb00547d9f28 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -19,6 +19,7 @@ #![cfg(test)] +use core::{cell::RefCell, marker::PhantomData}; use sp_core::H256; use sp_runtime::{ traits::{BadOrigin, BlakeTwo256, Dispatchable, IdentityLookup}, @@ -26,8 +27,13 @@ use sp_runtime::{ }; use frame_support::{ - assert_err_ignore_postinfo, assert_noop, assert_ok, parameter_types, - traits::{ConstU32, ConstU64, OnInitialize}, + assert_err_ignore_postinfo, assert_noop, assert_ok, + pallet_prelude::Pays, + parameter_types, + traits::{ + tokens::{ConversionFromAssetBalance, PaymentStatus}, + ConstU32, ConstU64, OnInitialize, + }, PalletId, }; @@ -96,10 +102,64 @@ impl pallet_utility::Config for Test { type WeightInfo = (); } +thread_local! { + pub static PAID: RefCell<BTreeMap<(u128, u32), u64>> = RefCell::new(BTreeMap::new()); + pub static STATUS: RefCell<BTreeMap<u64, PaymentStatus>> = RefCell::new(BTreeMap::new()); + pub static LAST_ID: RefCell<u64> = RefCell::new(0u64); +} + +/// paid balance for a given account and asset ids +fn paid(who: u128, asset_id: u32) -> u64 { + PAID.with(|p| p.borrow().get(&(who, asset_id)).cloned().unwrap_or(0)) +} + +/// reduce paid balance for a given account and asset ids +fn unpay(who: u128, asset_id: u32, amount: u64) { + PAID.with(|p| p.borrow_mut().entry((who, asset_id)).or_default().saturating_reduce(amount)) +} + +/// set status for a given payment id +fn set_status(id: u64, s: PaymentStatus) { + STATUS.with(|m| m.borrow_mut().insert(id, s)); +} + +pub struct TestPay; +impl Pay for TestPay { + type Beneficiary = u128; + type Balance = u64; + type Id = u64; + type AssetKind = u32; + type Error = (); + + fn pay( + who: &Self::Beneficiary, + asset_kind: Self::AssetKind, + amount: Self::Balance, + ) -> Result<Self::Id, Self::Error> { + PAID.with(|paid| *paid.borrow_mut().entry((*who, asset_kind)).or_default() += amount); + Ok(LAST_ID.with(|lid| { + let x = *lid.borrow(); + lid.replace(x + 1); + x + })) + } + fn check_payment(id: Self::Id) -> PaymentStatus { + STATUS.with(|s| s.borrow().get(&id).cloned().unwrap_or(PaymentStatus::Unknown)) + } + #[cfg(feature = "runtime-benchmarks")] + fn ensure_successful(_: &Self::Beneficiary, _: Self::AssetKind, _: Self::Balance) {} + #[cfg(feature = "runtime-benchmarks")] + fn ensure_concluded(id: Self::Id) { + set_status(id, PaymentStatus::Failure) + } +} + parameter_types! { pub const ProposalBond: Permill = Permill::from_percent(5); pub const Burn: Permill = Permill::from_percent(50); pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry"); + pub TreasuryAccount: u128 = Treasury::account_id(); + pub const SpendPayoutPeriod: u64 = 5; } pub struct TestSpendOrigin; impl frame_support::traits::EnsureOrigin<RuntimeOrigin> for TestSpendOrigin { @@ -120,6 +180,16 @@ impl frame_support::traits::EnsureOrigin<RuntimeOrigin> for TestSpendOrigin { } } +pub struct MulBy<N>(PhantomData<N>); +impl<N: Get<u64>> ConversionFromAssetBalance<u64, u32, u64> for MulBy<N> { + type Error = (); + fn from_asset_balance(balance: u64, _asset_id: u32) -> Result<u64, Self::Error> { + return balance.checked_mul(N::get()).ok_or(()) + } + #[cfg(feature = "runtime-benchmarks")] + fn ensure_successful(_: u32) {} +} + impl Config for Test { type PalletId = TreasuryPalletId; type Currency = pallet_balances::Pallet<Test>; @@ -137,6 +207,14 @@ impl Config for Test { type SpendFunds = (); type MaxApprovals = ConstU32<100>; type SpendOrigin = TestSpendOrigin; + type AssetKind = u32; + type Beneficiary = u128; + type BeneficiaryLookup = IdentityLookup<Self::Beneficiary>; + type Paymaster = TestPay; + type BalanceConverter = MulBy<ConstU64<2>>; + type PayoutPeriod = SpendPayoutPeriod; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); } pub fn new_test_ext() -> sp_io::TestExternalities { @@ -151,6 +229,14 @@ pub fn new_test_ext() -> sp_io::TestExternalities { t.into() } +fn get_payment_id(i: SpendIndex) -> Option<u64> { + let spend = Spends::<Test, _>::get(i).expect("no spend"); + match spend.status { + PaymentState::Attempted { id } => Some(id), + _ => None, + } +} + #[test] fn genesis_config_works() { new_test_ext().execute_with(|| { @@ -160,46 +246,49 @@ fn genesis_config_works() { } #[test] -fn spend_origin_permissioning_works() { +fn spend_local_origin_permissioning_works() { new_test_ext().execute_with(|| { - assert_noop!(Treasury::spend(RuntimeOrigin::signed(1), 1, 1), BadOrigin); + assert_noop!(Treasury::spend_local(RuntimeOrigin::signed(1), 1, 1), BadOrigin); assert_noop!( - Treasury::spend(RuntimeOrigin::signed(10), 6, 1), + Treasury::spend_local(RuntimeOrigin::signed(10), 6, 1), Error::<Test>::InsufficientPermission ); assert_noop!( - Treasury::spend(RuntimeOrigin::signed(11), 11, 1), + Treasury::spend_local(RuntimeOrigin::signed(11), 11, 1), Error::<Test>::InsufficientPermission ); assert_noop!( - Treasury::spend(RuntimeOrigin::signed(12), 21, 1), + Treasury::spend_local(RuntimeOrigin::signed(12), 21, 1), Error::<Test>::InsufficientPermission ); assert_noop!( - Treasury::spend(RuntimeOrigin::signed(13), 51, 1), + Treasury::spend_local(RuntimeOrigin::signed(13), 51, 1), Error::<Test>::InsufficientPermission ); }); } +#[docify::export] #[test] -fn spend_origin_works() { +fn spend_local_origin_works() { new_test_ext().execute_with(|| { // Check that accumulate works when we have Some value in Dummy already. Balances::make_free_balance_be(&Treasury::account_id(), 101); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), 5, 6)); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), 5, 6)); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), 5, 6)); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), 5, 6)); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(11), 10, 6)); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(12), 20, 6)); - assert_ok!(Treasury::spend(RuntimeOrigin::signed(13), 50, 6)); - + // approve spend of some amount to beneficiary `6`. + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(10), 5, 6)); + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(10), 5, 6)); + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(10), 5, 6)); + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(10), 5, 6)); + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(11), 10, 6)); + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(12), 20, 6)); + assert_ok!(Treasury::spend_local(RuntimeOrigin::signed(13), 50, 6)); + // free balance of `6` is zero, spend period has not passed. <Treasury as OnInitialize<u64>>::on_initialize(1); assert_eq!(Balances::free_balance(6), 0); - + // free balance of `6` is `100`, spend period has passed. <Treasury as OnInitialize<u64>>::on_initialize(2); assert_eq!(Balances::free_balance(6), 100); + // `100` spent, `1` burned. assert_eq!(Treasury::pot(), 0); }); } @@ -578,14 +667,49 @@ fn remove_already_removed_approval_fails() { }); } +#[test] +fn spending_local_in_batch_respects_max_total() { + new_test_ext().execute_with(|| { + // Respect the `max_total` for the given origin. + assert_ok!(RuntimeCall::from(UtilityCall::batch_all { + calls: vec![ + RuntimeCall::from(TreasuryCall::spend_local { amount: 2, beneficiary: 100 }), + RuntimeCall::from(TreasuryCall::spend_local { amount: 2, beneficiary: 101 }) + ] + }) + .dispatch(RuntimeOrigin::signed(10))); + + assert_err_ignore_postinfo!( + RuntimeCall::from(UtilityCall::batch_all { + calls: vec![ + RuntimeCall::from(TreasuryCall::spend_local { amount: 2, beneficiary: 100 }), + RuntimeCall::from(TreasuryCall::spend_local { amount: 4, beneficiary: 101 }) + ] + }) + .dispatch(RuntimeOrigin::signed(10)), + Error::<Test, _>::InsufficientPermission + ); + }) +} + #[test] fn spending_in_batch_respects_max_total() { new_test_ext().execute_with(|| { // Respect the `max_total` for the given origin. assert_ok!(RuntimeCall::from(UtilityCall::batch_all { calls: vec![ - RuntimeCall::from(TreasuryCall::spend { amount: 2, beneficiary: 100 }), - RuntimeCall::from(TreasuryCall::spend { amount: 2, beneficiary: 101 }) + RuntimeCall::from(TreasuryCall::spend { + asset_kind: Box::new(1), + amount: 1, + beneficiary: Box::new(100), + valid_from: None, + }), + RuntimeCall::from(TreasuryCall::spend { + asset_kind: Box::new(1), + amount: 1, + beneficiary: Box::new(101), + valid_from: None, + }) ] }) .dispatch(RuntimeOrigin::signed(10))); @@ -593,8 +717,18 @@ fn spending_in_batch_respects_max_total() { assert_err_ignore_postinfo!( RuntimeCall::from(UtilityCall::batch_all { calls: vec![ - RuntimeCall::from(TreasuryCall::spend { amount: 2, beneficiary: 100 }), - RuntimeCall::from(TreasuryCall::spend { amount: 4, beneficiary: 101 }) + RuntimeCall::from(TreasuryCall::spend { + asset_kind: Box::new(1), + amount: 2, + beneficiary: Box::new(100), + valid_from: None, + }), + RuntimeCall::from(TreasuryCall::spend { + asset_kind: Box::new(1), + amount: 2, + beneficiary: Box::new(101), + valid_from: None, + }) ] }) .dispatch(RuntimeOrigin::signed(10)), @@ -602,3 +736,251 @@ fn spending_in_batch_respects_max_total() { ); }) } + +#[test] +fn spend_origin_works() { + new_test_ext().execute_with(|| { + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None)); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_noop!( + Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 3, Box::new(6), None), + Error::<Test, _>::InsufficientPermission + ); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(11), Box::new(1), 5, Box::new(6), None)); + assert_noop!( + Treasury::spend(RuntimeOrigin::signed(11), Box::new(1), 6, Box::new(6), None), + Error::<Test, _>::InsufficientPermission + ); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(12), Box::new(1), 10, Box::new(6), None)); + assert_noop!( + Treasury::spend(RuntimeOrigin::signed(12), Box::new(1), 11, Box::new(6), None), + Error::<Test, _>::InsufficientPermission + ); + + assert_eq!(SpendCount::<Test, _>::get(), 4); + assert_eq!(Spends::<Test, _>::iter().count(), 4); + }); +} + +#[test] +fn spend_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + + assert_eq!(SpendCount::<Test, _>::get(), 1); + assert_eq!( + Spends::<Test, _>::get(0).unwrap(), + SpendStatus { + asset_kind: 1, + amount: 2, + beneficiary: 6, + valid_from: 1, + expire_at: 6, + status: PaymentState::Pending, + } + ); + System::assert_last_event( + Event::<Test, _>::AssetSpendApproved { + index: 0, + asset_kind: 1, + amount: 2, + beneficiary: 6, + valid_from: 1, + expire_at: 6, + } + .into(), + ); + }); +} + +#[test] +fn spend_expires() { + new_test_ext().execute_with(|| { + assert_eq!(<Test as Config>::PayoutPeriod::get(), 5); + + // spend `0` expires in 5 blocks after the creating. + System::set_block_number(1); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + System::set_block_number(6); + assert_noop!(Treasury::payout(RuntimeOrigin::signed(1), 0), Error::<Test, _>::SpendExpired); + + // spend cannot be approved since its already expired. + assert_noop!( + Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), Some(0)), + Error::<Test, _>::SpendExpired + ); + }); +} + +#[docify::export] +#[test] +fn spend_payout_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + // approve a `2` coins spend of asset `1` to beneficiary `6`, the spend valid from now. + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + // payout the spend. + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + // beneficiary received `2` coins of asset `1`. + assert_eq!(paid(6, 1), 2); + assert_eq!(SpendCount::<Test, _>::get(), 1); + let payment_id = get_payment_id(0).expect("no payment attempt"); + System::assert_last_event(Event::<Test, _>::Paid { index: 0, payment_id }.into()); + set_status(payment_id, PaymentStatus::Success); + // the payment succeed. + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); + System::assert_last_event(Event::<Test, _>::SpendProcessed { index: 0 }.into()); + // cannot payout the same spend twice. + assert_noop!(Treasury::payout(RuntimeOrigin::signed(1), 0), Error::<Test, _>::InvalidIndex); + }); +} + +#[test] +fn payout_retry_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_eq!(paid(6, 1), 2); + let payment_id = get_payment_id(0).expect("no payment attempt"); + // spend payment is failed + set_status(payment_id, PaymentStatus::Failure); + unpay(6, 1, 2); + // cannot payout a spend in the attempted state + assert_noop!( + Treasury::payout(RuntimeOrigin::signed(1), 0), + Error::<Test, _>::AlreadyAttempted + ); + // check status and update it to retry the payout again + assert_ok!(Treasury::check_status(RuntimeOrigin::signed(1), 0)); + System::assert_last_event(Event::<Test, _>::PaymentFailed { index: 0, payment_id }.into()); + // the payout can be retried now + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_eq!(paid(6, 1), 2); + }); +} + +#[test] +fn spend_valid_from_works() { + new_test_ext().execute_with(|| { + assert_eq!(<Test as Config>::PayoutPeriod::get(), 5); + System::set_block_number(1); + + // spend valid from block `2`. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 2, + Box::new(6), + Some(2) + )); + assert_noop!(Treasury::payout(RuntimeOrigin::signed(1), 0), Error::<Test, _>::EarlyPayout); + System::set_block_number(2); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + + System::set_block_number(5); + // spend approved even if `valid_from` in the past since the payout period has not passed. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 2, + Box::new(6), + Some(4) + )); + // spend paid. + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); + }); +} + +#[test] +fn void_spend_works() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + // spend cannot be voided if already attempted. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 2, + Box::new(6), + Some(1) + )); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); + assert_noop!( + Treasury::void_spend(RuntimeOrigin::root(), 0), + Error::<Test, _>::AlreadyAttempted + ); + + // void spend. + assert_ok!(Treasury::spend( + RuntimeOrigin::signed(10), + Box::new(1), + 2, + Box::new(6), + Some(10) + )); + assert_ok!(Treasury::void_spend(RuntimeOrigin::root(), 1)); + assert_eq!(Spends::<Test, _>::get(1), None); + }); +} + +#[test] +fn check_status_works() { + new_test_ext().execute_with(|| { + assert_eq!(<Test as Config>::PayoutPeriod::get(), 5); + System::set_block_number(1); + + // spend `0` expired and can be removed. + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + System::set_block_number(7); + let info = Treasury::check_status(RuntimeOrigin::signed(1), 0).unwrap(); + assert_eq!(info.pays_fee, Pays::No); + System::assert_last_event(Event::<Test, _>::SpendProcessed { index: 0 }.into()); + + // spend `1` payment failed and expired hence can be removed. + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_noop!( + Treasury::check_status(RuntimeOrigin::signed(1), 1), + Error::<Test, _>::NotAttempted + ); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 1)); + let payment_id = get_payment_id(1).expect("no payment attempt"); + set_status(payment_id, PaymentStatus::Failure); + // spend expired. + System::set_block_number(13); + let info = Treasury::check_status(RuntimeOrigin::signed(1), 1).unwrap(); + assert_eq!(info.pays_fee, Pays::Yes); + System::assert_last_event(Event::<Test, _>::PaymentFailed { index: 1, payment_id }.into()); + let info = Treasury::check_status(RuntimeOrigin::signed(1), 1).unwrap(); + assert_eq!(info.pays_fee, Pays::No); + System::assert_last_event(Event::<Test, _>::SpendProcessed { index: 1 }.into()); + + // spend `2` payment succeed. + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 2)); + let payment_id = get_payment_id(2).expect("no payment attempt"); + set_status(payment_id, PaymentStatus::Success); + let info = Treasury::check_status(RuntimeOrigin::signed(1), 2).unwrap(); + assert_eq!(info.pays_fee, Pays::No); + System::assert_last_event(Event::<Test, _>::SpendProcessed { index: 2 }.into()); + + // spend `3` payment in process. + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 3)); + let payment_id = get_payment_id(3).expect("no payment attempt"); + set_status(payment_id, PaymentStatus::InProgress); + assert_noop!( + Treasury::check_status(RuntimeOrigin::signed(1), 3), + Error::<Test, _>::Inconclusive + ); + + // spend `4` removed since the payment status is unknown. + assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); + assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 4)); + let payment_id = get_payment_id(4).expect("no payment attempt"); + set_status(payment_id, PaymentStatus::Unknown); + let info = Treasury::check_status(RuntimeOrigin::signed(1), 4).unwrap(); + assert_eq!(info.pays_fee, Pays::No); + System::assert_last_event(Event::<Test, _>::SpendProcessed { index: 4 }.into()); + }); +} diff --git a/substrate/frame/treasury/src/weights.rs b/substrate/frame/treasury/src/weights.rs index 8f1418f76d969..030e18980eb54 100644 --- a/substrate/frame/treasury/src/weights.rs +++ b/substrate/frame/treasury/src/weights.rs @@ -18,28 +18,23 @@ //! Autogenerated weights for pallet_treasury //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-06-16, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-07-07, STEPS: `20`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runner-e8ezs4ez-project-145-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 +//! HOSTNAME: `cob`, CPU: `<UNKNOWN>` +//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 // Executed Command: -// ./target/production/substrate +// ./target/debug/substrate // benchmark // pallet // --chain=dev -// --steps=50 -// --repeat=20 -// --pallet=pallet_treasury -// --no-storage-info -// --no-median-slopes -// --no-min-squares +// --steps=20 +// --repeat=2 +// --pallet=pallet-treasury // --extrinsic=* -// --execution=wasm // --wasm-execution=compiled // --heap-pages=4096 -// --output=./frame/treasury/src/weights.rs -// --header=./HEADER-APACHE2 +// --output=./frame/treasury/src/._weights.rs // --template=./.maintain/frame-weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] @@ -52,12 +47,16 @@ use core::marker::PhantomData; /// Weight functions needed for pallet_treasury. pub trait WeightInfo { - fn spend() -> Weight; + fn spend_local() -> Weight; fn propose_spend() -> Weight; fn reject_proposal() -> Weight; fn approve_proposal(p: u32, ) -> Weight; fn remove_approval() -> Weight; fn on_initialize_proposals(p: u32, ) -> Weight; + fn spend() -> Weight; + fn payout() -> Weight; + fn check_status() -> Weight; + fn void_spend() -> Weight; } /// Weights for pallet_treasury using the Substrate node and recommended hardware. @@ -69,12 +68,12 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> { /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) /// Storage: Treasury Proposals (r:0 w:1) /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) - fn spend() -> Weight { + fn spend_local() -> Weight { // Proof Size summary in bytes: // Measured: `76` // Estimated: `1887` - // Minimum execution time: 15_057_000 picoseconds. - Weight::from_parts(15_803_000, 1887) + // Minimum execution time: 179_000_000 picoseconds. + Weight::from_parts(190_000_000, 1887) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -86,8 +85,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> { // Proof Size summary in bytes: // Measured: `177` // Estimated: `1489` - // Minimum execution time: 28_923_000 picoseconds. - Weight::from_parts(29_495_000, 1489) + // Minimum execution time: 349_000_000 picoseconds. + Weight::from_parts(398_000_000, 1489) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -99,8 +98,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> { // Proof Size summary in bytes: // Measured: `335` // Estimated: `3593` - // Minimum execution time: 30_539_000 picoseconds. - Weight::from_parts(30_986_000, 3593) + // Minimum execution time: 367_000_000 picoseconds. + Weight::from_parts(388_000_000, 3593) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -111,12 +110,12 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> { /// The range of component `p` is `[0, 99]`. fn approve_proposal(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `504 + p * (8 ±0)` + // Measured: `483 + p * (9 ±0)` // Estimated: `3573` - // Minimum execution time: 9_320_000 picoseconds. - Weight::from_parts(12_606_599, 3573) - // Standard Error: 1_302 - .saturating_add(Weight::from_parts(71_054, 0).saturating_mul(p.into())) + // Minimum execution time: 111_000_000 picoseconds. + Weight::from_parts(108_813_243, 3573) + // Standard Error: 147_887 + .saturating_add(Weight::from_parts(683_216, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -126,8 +125,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> { // Proof Size summary in bytes: // Measured: `161` // Estimated: `1887` - // Minimum execution time: 7_231_000 picoseconds. - Weight::from_parts(7_459_000, 1887) + // Minimum execution time: 71_000_000 picoseconds. + Weight::from_parts(78_000_000, 1887) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -135,27 +134,81 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> { /// Proof: Treasury Deactivated (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) /// Storage: Treasury Approvals (r:1 w:1) /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) - /// Storage: Treasury Proposals (r:100 w:100) + /// Storage: Treasury Proposals (r:99 w:99) /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) - /// Storage: System Account (r:200 w:200) + /// Storage: System Account (r:198 w:198) /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) /// Storage: Bounties BountyApprovals (r:1 w:1) /// Proof: Bounties BountyApprovals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) - /// The range of component `p` is `[0, 100]`. + /// The range of component `p` is `[0, 99]`. fn on_initialize_proposals(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `421 + p * (251 ±0)` + // Measured: `427 + p * (251 ±0)` // Estimated: `1887 + p * (5206 ±0)` - // Minimum execution time: 44_769_000 picoseconds. - Weight::from_parts(57_915_572, 1887) - // Standard Error: 59_484 - .saturating_add(Weight::from_parts(42_343_732, 0).saturating_mul(p.into())) + // Minimum execution time: 614_000_000 picoseconds. + Weight::from_parts(498_501_558, 1887) + // Standard Error: 1_070_260 + .saturating_add(Weight::from_parts(599_011_690, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(p.into()))) .saturating_add(T::DbWeight::get().writes(3_u64)) .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(p.into()))) .saturating_add(Weight::from_parts(0, 5206).saturating_mul(p.into())) } + /// Storage: AssetRate ConversionRateToNative (r:1 w:0) + /// Proof: AssetRate ConversionRateToNative (max_values: None, max_size: Some(36), added: 2511, mode: MaxEncodedLen) + /// Storage: Treasury SpendCount (r:1 w:1) + /// Proof: Treasury SpendCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Treasury Spends (r:0 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(69), added: 2544, mode: MaxEncodedLen) + fn spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `140` + // Estimated: `3501` + // Minimum execution time: 214_000_000 picoseconds. + Weight::from_parts(216_000_000, 3501) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: Treasury Spends (r:1 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(69), added: 2544, mode: MaxEncodedLen) + /// Storage: Assets Asset (r:1 w:1) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:2 w:2) + /// Proof: Assets Account (max_values: None, max_size: Some(134), added: 2609, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn payout() -> Weight { + // Proof Size summary in bytes: + // Measured: `705` + // Estimated: `6208` + // Minimum execution time: 760_000_000 picoseconds. + Weight::from_parts(822_000_000, 6208) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: Treasury Spends (r:1 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(69), added: 2544, mode: MaxEncodedLen) + fn check_status() -> Weight { + // Proof Size summary in bytes: + // Measured: `194` + // Estimated: `3534` + // Minimum execution time: 153_000_000 picoseconds. + Weight::from_parts(160_000_000, 3534) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Treasury Spends (r:1 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(69), added: 2544, mode: MaxEncodedLen) + fn void_spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `194` + // Estimated: `3534` + // Minimum execution time: 147_000_000 picoseconds. + Weight::from_parts(181_000_000, 3534) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } } // For backwards compatibility and tests @@ -166,12 +219,12 @@ impl WeightInfo for () { /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) /// Storage: Treasury Proposals (r:0 w:1) /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) - fn spend() -> Weight { + fn spend_local() -> Weight { // Proof Size summary in bytes: // Measured: `76` // Estimated: `1887` - // Minimum execution time: 15_057_000 picoseconds. - Weight::from_parts(15_803_000, 1887) + // Minimum execution time: 179_000_000 picoseconds. + Weight::from_parts(190_000_000, 1887) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -183,8 +236,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `177` // Estimated: `1489` - // Minimum execution time: 28_923_000 picoseconds. - Weight::from_parts(29_495_000, 1489) + // Minimum execution time: 349_000_000 picoseconds. + Weight::from_parts(398_000_000, 1489) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -196,8 +249,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `335` // Estimated: `3593` - // Minimum execution time: 30_539_000 picoseconds. - Weight::from_parts(30_986_000, 3593) + // Minimum execution time: 367_000_000 picoseconds. + Weight::from_parts(388_000_000, 3593) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -208,12 +261,12 @@ impl WeightInfo for () { /// The range of component `p` is `[0, 99]`. fn approve_proposal(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `504 + p * (8 ±0)` + // Measured: `483 + p * (9 ±0)` // Estimated: `3573` - // Minimum execution time: 9_320_000 picoseconds. - Weight::from_parts(12_606_599, 3573) - // Standard Error: 1_302 - .saturating_add(Weight::from_parts(71_054, 0).saturating_mul(p.into())) + // Minimum execution time: 111_000_000 picoseconds. + Weight::from_parts(108_813_243, 3573) + // Standard Error: 147_887 + .saturating_add(Weight::from_parts(683_216, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -223,8 +276,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `161` // Estimated: `1887` - // Minimum execution time: 7_231_000 picoseconds. - Weight::from_parts(7_459_000, 1887) + // Minimum execution time: 71_000_000 picoseconds. + Weight::from_parts(78_000_000, 1887) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -232,25 +285,79 @@ impl WeightInfo for () { /// Proof: Treasury Deactivated (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) /// Storage: Treasury Approvals (r:1 w:1) /// Proof: Treasury Approvals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) - /// Storage: Treasury Proposals (r:100 w:100) + /// Storage: Treasury Proposals (r:99 w:99) /// Proof: Treasury Proposals (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) - /// Storage: System Account (r:200 w:200) + /// Storage: System Account (r:198 w:198) /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) /// Storage: Bounties BountyApprovals (r:1 w:1) /// Proof: Bounties BountyApprovals (max_values: Some(1), max_size: Some(402), added: 897, mode: MaxEncodedLen) - /// The range of component `p` is `[0, 100]`. + /// The range of component `p` is `[0, 99]`. fn on_initialize_proposals(p: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `421 + p * (251 ±0)` + // Measured: `427 + p * (251 ±0)` // Estimated: `1887 + p * (5206 ±0)` - // Minimum execution time: 44_769_000 picoseconds. - Weight::from_parts(57_915_572, 1887) - // Standard Error: 59_484 - .saturating_add(Weight::from_parts(42_343_732, 0).saturating_mul(p.into())) + // Minimum execution time: 614_000_000 picoseconds. + Weight::from_parts(498_501_558, 1887) + // Standard Error: 1_070_260 + .saturating_add(Weight::from_parts(599_011_690, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().reads((3_u64).saturating_mul(p.into()))) .saturating_add(RocksDbWeight::get().writes(3_u64)) .saturating_add(RocksDbWeight::get().writes((3_u64).saturating_mul(p.into()))) .saturating_add(Weight::from_parts(0, 5206).saturating_mul(p.into())) } + /// Storage: AssetRate ConversionRateToNative (r:1 w:0) + /// Proof: AssetRate ConversionRateToNative (max_values: None, max_size: Some(36), added: 2511, mode: MaxEncodedLen) + /// Storage: Treasury SpendCount (r:1 w:1) + /// Proof: Treasury SpendCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Treasury Spends (r:0 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(69), added: 2544, mode: MaxEncodedLen) + fn spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `140` + // Estimated: `3501` + // Minimum execution time: 214_000_000 picoseconds. + Weight::from_parts(216_000_000, 3501) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: Treasury Spends (r:1 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(69), added: 2544, mode: MaxEncodedLen) + /// Storage: Assets Asset (r:1 w:1) + /// Proof: Assets Asset (max_values: None, max_size: Some(210), added: 2685, mode: MaxEncodedLen) + /// Storage: Assets Account (r:2 w:2) + /// Proof: Assets Account (max_values: None, max_size: Some(134), added: 2609, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn payout() -> Weight { + // Proof Size summary in bytes: + // Measured: `705` + // Estimated: `6208` + // Minimum execution time: 760_000_000 picoseconds. + Weight::from_parts(822_000_000, 6208) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: Treasury Spends (r:1 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(69), added: 2544, mode: MaxEncodedLen) + fn check_status() -> Weight { + // Proof Size summary in bytes: + // Measured: `194` + // Estimated: `3534` + // Minimum execution time: 153_000_000 picoseconds. + Weight::from_parts(160_000_000, 3534) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Treasury Spends (r:1 w:1) + /// Proof: Treasury Spends (max_values: None, max_size: Some(69), added: 2544, mode: MaxEncodedLen) + fn void_spend() -> Weight { + // Proof Size summary in bytes: + // Measured: `194` + // Estimated: `3534` + // Minimum execution time: 147_000_000 picoseconds. + Weight::from_parts(181_000_000, 3534) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } } diff --git a/substrate/primitives/core/src/crypto.rs b/substrate/primitives/core/src/crypto.rs index 8c7d98f00cd89..be9f56eb2ba4d 100644 --- a/substrate/primitives/core/src/crypto.rs +++ b/substrate/primitives/core/src/crypto.rs @@ -630,6 +630,13 @@ impl sp_std::str::FromStr for AccountId32 { } } +/// Creates an [`AccountId32`] from the input, which should contain at least 32 bytes. +impl FromEntropy for AccountId32 { + fn from_entropy(input: &mut impl codec::Input) -> Result<Self, codec::Error> { + Ok(AccountId32::new(FromEntropy::from_entropy(input)?)) + } +} + #[cfg(feature = "std")] pub use self::dummy::*; @@ -1171,6 +1178,13 @@ impl FromEntropy for bool { } } +/// Create the unit type for any given input. +impl FromEntropy for () { + fn from_entropy(_: &mut impl codec::Input) -> Result<Self, codec::Error> { + Ok(()) + } +} + macro_rules! impl_from_entropy { ($type:ty , $( $others:tt )*) => { impl_from_entropy!($type); From 1dc935c715ac715ac8f8fce2907ef8b2af2c8c38 Mon Sep 17 00:00:00 2001 From: Ignacio Palacios <ignacio.palacios.santos@gmail.com> Date: Mon, 9 Oct 2023 12:15:30 +0200 Subject: [PATCH 16/54] [xcm-emulator] Decouple the `AccountId` type from `AccountId32` (#1458) Closes: #1381 Originally from: https://github.com/paritytech/cumulus/pull/3037 --------- Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> --- cumulus/xcm/xcm-emulator/src/lib.rs | 57 +++++++++++++++-------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/cumulus/xcm/xcm-emulator/src/lib.rs b/cumulus/xcm/xcm-emulator/src/lib.rs index 9fda0632bae40..caf73ae1e41ca 100644 --- a/cumulus/xcm/xcm-emulator/src/lib.rs +++ b/cumulus/xcm/xcm-emulator/src/lib.rs @@ -26,7 +26,7 @@ pub use std::{ // Substrate pub use frame_support::{ assert_ok, - sp_runtime::{traits::Header as HeaderT, AccountId32, DispatchResult}, + sp_runtime::{traits::Header as HeaderT, DispatchResult}, traits::{ EnqueueMessage, Get, Hooks, OriginTrait, ProcessMessage, ProcessMessageError, ServiceQueues, }, @@ -61,6 +61,8 @@ pub use xcm::v3::prelude::{ }; pub use xcm_executor::traits::ConvertLocation; +pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId; + thread_local! { /// Downward messages, each message is: `(to_para_id, [(relay_block_number, msg)])` #[allow(clippy::type_complexity)] @@ -90,8 +92,8 @@ pub trait CheckAssertion<Origin, Destination, Hops, Args> where Origin: Chain + Clone, Destination: Chain + Clone, - Origin::RuntimeOrigin: OriginTrait<AccountId = AccountId32> + Clone, - Destination::RuntimeOrigin: OriginTrait<AccountId = AccountId32> + Clone, + Origin::RuntimeOrigin: OriginTrait<AccountId = AccountIdOf<Origin::Runtime>> + Clone, + Destination::RuntimeOrigin: OriginTrait<AccountId = AccountIdOf<Destination::Runtime>> + Clone, Hops: Clone, Args: Clone, { @@ -103,8 +105,8 @@ impl<Origin, Destination, Hops, Args> CheckAssertion<Origin, Destination, Hops, where Origin: Chain + Clone, Destination: Chain + Clone, - Origin::RuntimeOrigin: OriginTrait<AccountId = AccountId32> + Clone, - Destination::RuntimeOrigin: OriginTrait<AccountId = AccountId32> + Clone, + Origin::RuntimeOrigin: OriginTrait<AccountId = AccountIdOf<Origin::Runtime>> + Clone, + Destination::RuntimeOrigin: OriginTrait<AccountId = AccountIdOf<Destination::Runtime>> + Clone, Hops: Clone, Args: Clone, { @@ -219,24 +221,24 @@ pub trait Chain: TestExt + NetworkComponent { helpers::get_account_id_from_seed::<sr25519::Public>(seed) } - fn account_data_of(account: AccountId) -> AccountData<Balance>; + fn account_data_of(account: AccountIdOf<Self::Runtime>) -> AccountData<Balance>; fn events() -> Vec<<Self as Chain>::RuntimeEvent>; } pub trait RelayChain: Chain { type MessageProcessor: ProcessMessage; - type SovereignAccountOf: ConvertLocation<AccountId>; + type SovereignAccountOf: ConvertLocation<AccountIdOf<Self::Runtime>>; fn child_location_of(id: ParaId) -> MultiLocation { (Ancestor(0), ParachainJunction(id.into())).into() } - fn sovereign_account_id_of(location: MultiLocation) -> AccountId { + fn sovereign_account_id_of(location: MultiLocation) -> AccountIdOf<Self::Runtime> { Self::SovereignAccountOf::convert_location(&location).unwrap() } - fn sovereign_account_id_of_child_para(id: ParaId) -> AccountId { + fn sovereign_account_id_of_child_para(id: ParaId) -> AccountIdOf<Self::Runtime> { Self::sovereign_account_id_of(Self::child_location_of(id)) } } @@ -244,7 +246,7 @@ pub trait RelayChain: Chain { pub trait Parachain: Chain { type XcmpMessageHandler: XcmpMessageHandler; type DmpMessageHandler: DmpMessageHandler; - type LocationToAccountId: ConvertLocation<AccountId>; + type LocationToAccountId: ConvertLocation<AccountIdOf<Self::Runtime>>; type ParachainInfo: Get<ParaId>; type ParachainSystem; @@ -268,7 +270,7 @@ pub trait Parachain: Chain { (Parent, X1(ParachainJunction(para_id.into()))).into() } - fn sovereign_account_id_of(location: MultiLocation) -> AccountId { + fn sovereign_account_id_of(location: MultiLocation) -> AccountIdOf<Self::Runtime> { Self::LocationToAccountId::convert_location(&location).unwrap() } } @@ -365,7 +367,7 @@ macro_rules! decl_test_relay_chains { type RuntimeEvent = $runtime::RuntimeEvent; type System = $crate::SystemPallet::<Self::Runtime>; - fn account_data_of(account: $crate::AccountId) -> $crate::AccountData<$crate::Balance> { + fn account_data_of(account: $crate::AccountIdOf<Self::Runtime>) -> $crate::AccountData<$crate::Balance> { <Self as $crate::TestExt>::ext_wrapper(|| $crate::SystemPallet::<Self::Runtime>::account(account).data.into()) } @@ -590,7 +592,7 @@ macro_rules! decl_test_parachains { type RuntimeEvent = $runtime::RuntimeEvent; type System = $crate::SystemPallet::<Self::Runtime>; - fn account_data_of(account: $crate::AccountId) -> $crate::AccountData<$crate::Balance> { + fn account_data_of(account: $crate::AccountIdOf<Self::Runtime>) -> $crate::AccountData<$crate::Balance> { <Self as $crate::TestExt>::ext_wrapper(|| $crate::SystemPallet::<Self::Runtime>::account(account).data.into()) } @@ -1159,9 +1161,10 @@ macro_rules! __impl_check_assertion { where Origin: $crate::Chain + Clone, Destination: $crate::Chain + Clone, - Origin::RuntimeOrigin: $crate::OriginTrait<AccountId = $crate::AccountId32> + Clone, + Origin::RuntimeOrigin: + $crate::OriginTrait<AccountId = $crate::AccountIdOf<Origin::Runtime>> + Clone, Destination::RuntimeOrigin: - $crate::OriginTrait<AccountId = $crate::AccountId32> + Clone, + $crate::OriginTrait<AccountId = $crate::AccountIdOf<Destination::Runtime>> + Clone, Hops: Clone, Args: Clone, { @@ -1308,8 +1311,8 @@ where /// Struct that keeps account's id and balance #[derive(Clone)] -pub struct TestAccount { - pub account_id: AccountId, +pub struct TestAccount<R: Chain> { + pub account_id: AccountIdOf<R::Runtime>, pub balance: Balance, } @@ -1326,9 +1329,9 @@ pub struct TestArgs { } /// Auxiliar struct to help creating a new `Test` instance -pub struct TestContext<T> { - pub sender: AccountId, - pub receiver: AccountId, +pub struct TestContext<T, Origin: Chain, Destination: Chain> { + pub sender: AccountIdOf<Origin::Runtime>, + pub receiver: AccountIdOf<Destination::Runtime>, pub args: T, } @@ -1345,12 +1348,12 @@ pub struct Test<Origin, Destination, Hops = (), Args = TestArgs> where Origin: Chain + Clone, Destination: Chain + Clone, - Origin::RuntimeOrigin: OriginTrait<AccountId = AccountId32> + Clone, - Destination::RuntimeOrigin: OriginTrait<AccountId = AccountId32> + Clone, + Origin::RuntimeOrigin: OriginTrait<AccountId = AccountIdOf<Origin::Runtime>> + Clone, + Destination::RuntimeOrigin: OriginTrait<AccountId = AccountIdOf<Destination::Runtime>> + Clone, Hops: Clone, { - pub sender: TestAccount, - pub receiver: TestAccount, + pub sender: TestAccount<Origin>, + pub receiver: TestAccount<Destination>, pub signed_origin: Origin::RuntimeOrigin, pub root_origin: Origin::RuntimeOrigin, pub hops_assertion: HashMap<String, fn(Self)>, @@ -1365,12 +1368,12 @@ where Args: Clone, Origin: Chain + Clone + CheckAssertion<Origin, Destination, Hops, Args>, Destination: Chain + Clone + CheckAssertion<Origin, Destination, Hops, Args>, - Origin::RuntimeOrigin: OriginTrait<AccountId = AccountId32> + Clone, - Destination::RuntimeOrigin: OriginTrait<AccountId = AccountId32> + Clone, + Origin::RuntimeOrigin: OriginTrait<AccountId = AccountIdOf<Origin::Runtime>> + Clone, + Destination::RuntimeOrigin: OriginTrait<AccountId = AccountIdOf<Destination::Runtime>> + Clone, Hops: Clone + CheckAssertion<Origin, Destination, Hops, Args>, { /// Creates a new `Test` instance - pub fn new(test_args: TestContext<Args>) -> Self { + pub fn new(test_args: TestContext<Args, Origin, Destination>) -> Self { Test { sender: TestAccount { account_id: test_args.sender.clone(), From a808a3a0918ffbce314dbe00e03761e7a8f8ce79 Mon Sep 17 00:00:00 2001 From: David Emett <david.emett@parity.io> Date: Mon, 9 Oct 2023 15:56:30 +0200 Subject: [PATCH 17/54] Mixnet integration (#1346) See #1345, <https://github.com/paritytech/substrate/pull/14207>. This adds all the necessary mixnet components, and puts them together in the "kitchen-sink" node/runtime. The components added are: - A pallet (`frame/mixnet`). This is responsible for determining the current mixnet session and phase, and the mixnodes to use in each session. It provides a function that validators can call to register a mixnode for the next session. The logic of this pallet is very similar to that of the `im-online` pallet. - A service (`client/mixnet`). This implements the core mixnet logic, building on the `mixnet` crate. The service communicates with other nodes using notifications sent over the "mixnet" protocol. - An RPC interface. This currently only supports sending transactions over the mixnet. --------- Co-authored-by: David Emett <dave@sp4m.net> Co-authored-by: Javier Viola <javier@parity.io> --- Cargo.lock | 311 +++++++-- Cargo.toml | 3 + substrate/bin/node/cli/Cargo.toml | 2 + .../bin/node/cli/benches/block_production.rs | 2 +- .../bin/node/cli/benches/transaction_pool.rs | 2 +- substrate/bin/node/cli/src/chain_spec.rs | 32 +- substrate/bin/node/cli/src/cli.rs | 4 + substrate/bin/node/cli/src/command.rs | 19 +- substrate/bin/node/cli/src/service.rs | 66 +- substrate/bin/node/rpc/Cargo.toml | 1 + substrate/bin/node/rpc/src/lib.rs | 9 + substrate/bin/node/runtime/Cargo.toml | 6 + substrate/bin/node/runtime/src/lib.rs | 45 +- substrate/bin/node/testing/src/genesis.rs | 1 + substrate/bin/node/testing/src/keyring.rs | 1 + .../bin/utils/chain-spec-builder/src/lib.rs | 4 +- substrate/client/cli/Cargo.toml | 1 + .../client/cli/src/params/mixnet_params.rs | 67 ++ substrate/client/cli/src/params/mod.rs | 8 +- substrate/client/mixnet/Cargo.toml | 36 ++ substrate/client/mixnet/README.md | 3 + substrate/client/mixnet/src/api.rs | 69 ++ substrate/client/mixnet/src/config.rs | 88 +++ substrate/client/mixnet/src/error.rs | 56 ++ .../client/mixnet/src/extrinsic_queue.rs | 94 +++ substrate/client/mixnet/src/lib.rs | 44 ++ .../client/mixnet/src/maybe_inf_delay.rs | 111 ++++ .../client/mixnet/src/packet_dispatcher.rs | 198 ++++++ substrate/client/mixnet/src/peer_id.rs | 44 ++ substrate/client/mixnet/src/protocol.rs | 42 ++ substrate/client/mixnet/src/request.rs | 119 ++++ substrate/client/mixnet/src/run.rs | 388 ++++++++++++ .../client/mixnet/src/sync_with_runtime.rs | 228 +++++++ .../client/network/src/protocol_controller.rs | 13 +- substrate/client/rpc-api/Cargo.toml | 1 + substrate/client/rpc-api/src/error.rs | 1 + substrate/client/rpc-api/src/lib.rs | 1 + substrate/client/rpc-api/src/mixnet/error.rs | 48 ++ substrate/client/rpc-api/src/mixnet/mod.rs | 31 + substrate/client/rpc/Cargo.toml | 1 + substrate/client/rpc/src/lib.rs | 1 + substrate/client/rpc/src/mixnet/mod.rs | 47 ++ substrate/frame/mixnet/Cargo.toml | 57 ++ substrate/frame/mixnet/README.md | 4 + substrate/frame/mixnet/src/lib.rs | 598 ++++++++++++++++++ substrate/primitives/core/src/crypto.rs | 2 + substrate/primitives/mixnet/Cargo.toml | 30 + substrate/primitives/mixnet/README.md | 3 + substrate/primitives/mixnet/src/lib.rs | 24 + .../primitives/mixnet/src/runtime_api.rs | 52 ++ substrate/primitives/mixnet/src/types.rs | 100 +++ substrate/scripts/ci/deny.toml | 1 + 52 files changed, 3010 insertions(+), 109 deletions(-) create mode 100644 substrate/client/cli/src/params/mixnet_params.rs create mode 100644 substrate/client/mixnet/Cargo.toml create mode 100644 substrate/client/mixnet/README.md create mode 100644 substrate/client/mixnet/src/api.rs create mode 100644 substrate/client/mixnet/src/config.rs create mode 100644 substrate/client/mixnet/src/error.rs create mode 100644 substrate/client/mixnet/src/extrinsic_queue.rs create mode 100644 substrate/client/mixnet/src/lib.rs create mode 100644 substrate/client/mixnet/src/maybe_inf_delay.rs create mode 100644 substrate/client/mixnet/src/packet_dispatcher.rs create mode 100644 substrate/client/mixnet/src/peer_id.rs create mode 100644 substrate/client/mixnet/src/protocol.rs create mode 100644 substrate/client/mixnet/src/request.rs create mode 100644 substrate/client/mixnet/src/run.rs create mode 100644 substrate/client/mixnet/src/sync_with_runtime.rs create mode 100644 substrate/client/rpc-api/src/mixnet/error.rs create mode 100644 substrate/client/rpc-api/src/mixnet/mod.rs create mode 100644 substrate/client/rpc/src/mixnet/mod.rs create mode 100644 substrate/frame/mixnet/Cargo.toml create mode 100644 substrate/frame/mixnet/README.md create mode 100644 substrate/frame/mixnet/src/lib.rs create mode 100644 substrate/primitives/mixnet/Cargo.toml create mode 100644 substrate/primitives/mixnet/README.md create mode 100644 substrate/primitives/mixnet/src/lib.rs create mode 100644 substrate/primitives/mixnet/src/runtime_api.rs create mode 100644 substrate/primitives/mixnet/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 48ac004a85970..cc80e2542fb06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,7 +116,7 @@ dependencies = [ "cipher 0.3.0", "ctr 0.8.0", "ghash 0.4.4", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -130,7 +130,7 @@ dependencies = [ "cipher 0.4.4", "ctr 0.9.2", "ghash 0.5.0", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -571,6 +571,12 @@ dependencies = [ "sha3", ] +[[package]] +name = "array-bytes" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52f63c5c1316a16a4b35eaac8b76a98248961a533f061684cb2a7cb0eafb6c6" + [[package]] name = "array-bytes" version = "6.1.0" @@ -1276,7 +1282,7 @@ dependencies = [ name = "binary-merkle-tree" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "env_logger 0.9.3", "hash-db", "log", @@ -1353,6 +1359,18 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" +dependencies = [ + "byte-tools", + "crypto-mac 0.7.0", + "digest 0.8.1", + "opaque-debug 0.2.3", +] + [[package]] name = "blake2" version = "0.10.6" @@ -2180,6 +2198,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "c2-chacha" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27dae93fe7b1e0424dc57179ac396908c26b035a87234809f5c4dfd1b47dc80" +dependencies = [ + "cipher 0.2.5", + "ppv-lite86", +] + [[package]] name = "camino" version = "1.1.6" @@ -2236,7 +2264,7 @@ checksum = "5aca1a8fbc20b50ac9673ff014abfb2b5f4085ee1a850d408f14a159c5853ac7" dependencies = [ "aead 0.3.2", "cipher 0.2.5", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -2269,6 +2297,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862" +dependencies = [ + "byteorder", + "keystream", +] + [[package]] name = "chacha20" version = "0.8.2" @@ -3134,7 +3172,7 @@ checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" dependencies = [ "generic-array 0.14.7", "rand_core 0.6.4", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -3146,7 +3184,7 @@ checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" dependencies = [ "generic-array 0.14.7", "rand_core 0.6.4", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -3161,6 +3199,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +dependencies = [ + "generic-array 0.12.4", + "subtle 1.0.0", +] + [[package]] name = "crypto-mac" version = "0.8.0" @@ -3168,7 +3216,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" dependencies = [ "generic-array 0.14.7", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -3178,7 +3226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ "generic-array 0.14.7", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -3741,7 +3789,7 @@ dependencies = [ name = "cumulus-relay-chain-minimal-node" version = "0.1.0" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "async-trait", "cumulus-primitives-core", "cumulus-relay-chain-interface", @@ -3968,7 +4016,7 @@ dependencies = [ "byteorder", "digest 0.8.1", "rand_core 0.5.1", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -3981,7 +4029,7 @@ dependencies = [ "byteorder", "digest 0.9.0", "rand_core 0.5.1", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -3998,7 +4046,7 @@ dependencies = [ "fiat-crypto", "platforms", "rustc_version 0.4.0", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -4313,7 +4361,7 @@ dependencies = [ "block-buffer 0.10.4", "const-oid", "crypto-common", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -4582,7 +4630,7 @@ dependencies = [ "pkcs8 0.9.0", "rand_core 0.6.4", "sec1 0.3.0", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -4601,7 +4649,7 @@ dependencies = [ "pkcs8 0.10.2", "rand_core 0.6.4", "sec1 0.7.3", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -4801,7 +4849,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f86a749cf851891866c10515ef6c299b5c69661465e9c3bbe7e07a2b77fb0f7" dependencies = [ - "blake2", + "blake2 0.10.6", "fs-err", "proc-macro2", "quote", @@ -4902,7 +4950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" dependencies = [ "rand_core 0.6.4", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -4912,7 +4960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ "rand_core 0.6.4", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -5065,7 +5113,7 @@ checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" name = "frame-benchmarking" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "frame-support", "frame-support-procedural", "frame-system", @@ -5093,7 +5141,7 @@ name = "frame-benchmarking-cli" version = "4.0.0-dev" dependencies = [ "Inflector", - "array-bytes", + "array-bytes 6.1.0", "chrono", "clap 4.4.6", "comfy-table", @@ -5204,7 +5252,7 @@ dependencies = [ name = "frame-executive" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "frame-support", "frame-system", "frame-try-runtime", @@ -5261,7 +5309,7 @@ name = "frame-support" version = "4.0.0-dev" dependencies = [ "aquamarine", - "array-bytes", + "array-bytes 6.1.0", "assert_matches", "bitflags 1.3.2", "docify", @@ -5791,7 +5839,7 @@ checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ "ff 0.12.1", "rand_core 0.6.4", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -5802,7 +5850,7 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff 0.13.0", "rand_core 0.6.4", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -5888,6 +5936,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.0", +] + [[package]] name = "heck" version = "0.4.1" @@ -6663,6 +6720,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "keystream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" + [[package]] name = "kitchensink-runtime" version = "3.0.0-dev" @@ -6711,6 +6774,7 @@ dependencies = [ "pallet-lottery", "pallet-membership", "pallet-message-queue", + "pallet-mixnet", "pallet-mmr", "pallet-multisig", "pallet-nft-fractionalization", @@ -6764,6 +6828,7 @@ dependencies = [ "sp-genesis-builder", "sp-inherents", "sp-io", + "sp-mixnet", "sp-offchain", "sp-runtime", "sp-session", @@ -7366,7 +7431,7 @@ checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" dependencies = [ "crunchy", "digest 0.9.0", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -7449,6 +7514,18 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +[[package]] +name = "lioness" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9" +dependencies = [ + "arrayref", + "blake2 0.8.1", + "chacha", + "keystream", +] + [[package]] name = "lite-json" version = "0.2.0" @@ -7779,6 +7856,31 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mixnet" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa3eb39495d8e2e2947a1d862852c90cc6a4a8845f8b41c8829cb9fcc047f4a" +dependencies = [ + "arrayref", + "arrayvec 0.7.4", + "bitflags 1.3.2", + "blake2 0.10.6", + "c2-chacha", + "curve25519-dalek 4.0.0", + "either", + "hashlink", + "lioness", + "log", + "parking_lot 0.12.1", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_distr", + "subtle 2.4.1", + "thiserror", + "zeroize", +] + [[package]] name = "mmr-gadget" version = "4.0.0-dev" @@ -8080,7 +8182,7 @@ checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" name = "node-bench" version = "0.9.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "clap 4.4.6", "derive_more", "fs_extra", @@ -8116,7 +8218,7 @@ dependencies = [ name = "node-cli" version = "3.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "assert_cmd", "clap 4.4.6", "clap_complete", @@ -8157,6 +8259,7 @@ dependencies = [ "sc-consensus-slots", "sc-executor", "sc-keystore", + "sc-mixnet", "sc-network", "sc-network-common", "sc-network-statement", @@ -8186,6 +8289,7 @@ dependencies = [ "sp-io", "sp-keyring", "sp-keystore", + "sp-mixnet", "sp-runtime", "sp-statement-store", "sp-timestamp", @@ -8277,6 +8381,7 @@ dependencies = [ "sc-consensus-babe-rpc", "sc-consensus-grandpa", "sc-consensus-grandpa-rpc", + "sc-mixnet", "sc-rpc", "sc-rpc-api", "sc-rpc-spec-v2", @@ -8723,7 +8828,7 @@ dependencies = [ name = "pallet-alliance" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "frame-benchmarking", "frame-support", "frame-system", @@ -9026,7 +9131,7 @@ dependencies = [ name = "pallet-beefy-mmr" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "binary-merkle-tree", "frame-support", "frame-system", @@ -9249,7 +9354,7 @@ dependencies = [ name = "pallet-contracts" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "assert_matches", "bitflags 1.3.2", "env_logger 0.9.3", @@ -9583,7 +9688,7 @@ dependencies = [ name = "pallet-glutton" version = "4.0.0-dev" dependencies = [ - "blake2", + "blake2 0.10.6", "frame-benchmarking", "frame-support", "frame-system", @@ -9751,11 +9856,30 @@ dependencies = [ "sp-weights", ] +[[package]] +name = "pallet-mixnet" +version = "0.1.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "serde", + "sp-application-crypto", + "sp-arithmetic", + "sp-io", + "sp-mixnet", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-mmr" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "env_logger 0.9.3", "frame-benchmarking", "frame-support", @@ -10553,7 +10677,7 @@ dependencies = [ name = "pallet-transaction-storage" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "frame-benchmarking", "frame-support", "frame-system", @@ -10946,7 +11070,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78f19d20a0d2cc52327a88d131fa1c4ea81ea4a04714aedcfeca2dd410049cf8" dependencies = [ - "blake2", + "blake2 0.10.6", "crc32fast", "fs2", "hex", @@ -13787,7 +13911,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ "hmac 0.12.1", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -13800,7 +13924,7 @@ dependencies = [ "ark-poly", "ark-serialize", "ark-std", - "blake2", + "blake2 0.10.6", "common", "fflonk", "merlin 3.0.0", @@ -14420,7 +14544,7 @@ dependencies = [ name = "sc-cli" version = "0.10.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "chrono", "clap 4.4.6", "fdlimit", @@ -14436,6 +14560,7 @@ dependencies = [ "sc-client-api", "sc-client-db", "sc-keystore", + "sc-mixnet", "sc-network", "sc-service", "sc-telemetry", @@ -14490,7 +14615,7 @@ dependencies = [ name = "sc-client-db" version = "0.10.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "criterion 0.4.0", "hash-db", "kitchensink-runtime", @@ -14655,7 +14780,7 @@ dependencies = [ name = "sc-consensus-beefy" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "async-channel", "async-trait", "fnv", @@ -14731,7 +14856,7 @@ name = "sc-consensus-grandpa" version = "0.10.0-dev" dependencies = [ "ahash 0.8.3", - "array-bytes", + "array-bytes 6.1.0", "assert_matches", "async-trait", "dyn-clone", @@ -14886,7 +15011,7 @@ dependencies = [ name = "sc-executor" version = "0.10.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "assert_matches", "criterion 0.4.0", "env_logger 0.9.3", @@ -14973,7 +15098,7 @@ dependencies = [ name = "sc-keystore" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "parking_lot 0.12.1", "serde_json", "sp-application-crypto", @@ -14983,11 +15108,38 @@ dependencies = [ "thiserror", ] +[[package]] +name = "sc-mixnet" +version = "0.1.0-dev" +dependencies = [ + "array-bytes 4.2.0", + "arrayvec 0.7.4", + "blake2 0.10.6", + "futures", + "futures-timer", + "libp2p-identity", + "log", + "mixnet", + "multiaddr", + "parity-scale-codec", + "parking_lot 0.12.1", + "sc-client-api", + "sc-network", + "sc-transaction-pool-api", + "sp-api", + "sp-consensus", + "sp-core", + "sp-keystore", + "sp-mixnet", + "sp-runtime", + "thiserror", +] + [[package]] name = "sc-network" version = "0.10.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "assert_matches", "async-channel", "async-trait", @@ -15102,7 +15254,7 @@ dependencies = [ name = "sc-network-light" version = "0.10.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "async-channel", "futures", "libp2p-identity", @@ -15122,7 +15274,7 @@ dependencies = [ name = "sc-network-statement" version = "0.10.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "async-channel", "futures", "libp2p", @@ -15139,7 +15291,7 @@ dependencies = [ name = "sc-network-sync" version = "0.10.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "async-channel", "async-trait", "fork-tree", @@ -15209,7 +15361,7 @@ dependencies = [ name = "sc-network-transactions" version = "0.10.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "futures", "libp2p", "log", @@ -15226,7 +15378,7 @@ dependencies = [ name = "sc-offchain" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "bytes", "fnv", "futures", @@ -15286,6 +15438,7 @@ dependencies = [ "sc-block-builder", "sc-chain-spec", "sc-client-api", + "sc-mixnet", "sc-network", "sc-network-common", "sc-rpc-api", @@ -15317,6 +15470,7 @@ dependencies = [ "jsonrpsee", "parity-scale-codec", "sc-chain-spec", + "sc-mixnet", "sc-transaction-pool-api", "scale-info", "serde", @@ -15346,7 +15500,7 @@ dependencies = [ name = "sc-rpc-spec-v2" version = "0.10.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "assert_matches", "futures", "futures-util", @@ -15459,7 +15613,7 @@ dependencies = [ name = "sc-service-test" version = "2.0.0" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "async-channel", "fdlimit", "futures", @@ -15632,7 +15786,7 @@ dependencies = [ name = "sc-transaction-pool" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "assert_matches", "async-trait", "criterion 0.4.0", @@ -15752,7 +15906,7 @@ dependencies = [ "rand 0.7.3", "rand_core 0.5.1", "sha2 0.8.2", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -15826,7 +15980,7 @@ dependencies = [ "der 0.6.1", "generic-array 0.14.7", "pkcs8 0.9.0", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -15840,7 +15994,7 @@ dependencies = [ "der 0.7.8", "generic-array 0.14.7", "pkcs8 0.10.2", - "subtle", + "subtle 2.4.1", "zeroize", ] @@ -16405,14 +16559,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9d1425eb528a21de2755c75af4c9b5d57f50a0d4c3b7f1828a4cd03f8ba155" dependencies = [ "aes-gcm 0.9.4", - "blake2", + "blake2 0.10.6", "chacha20poly1305", "curve25519-dalek 4.0.0", "rand_core 0.6.4", "ring 0.16.20", "rustc_version 0.4.0", "sha2 0.10.7", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -16479,7 +16633,7 @@ version = "4.0.0-dev" dependencies = [ "Inflector", "assert_matches", - "blake2", + "blake2 0.10.6", "expander 2.0.0", "proc-macro-crate", "proc-macro2", @@ -16747,7 +16901,7 @@ dependencies = [ name = "sp-consensus-beefy" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "lazy_static", "parity-scale-codec", "scale-info", @@ -16821,10 +16975,10 @@ dependencies = [ name = "sp-core" version = "21.0.0" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "bandersnatch_vrfs", "bitflags 1.3.2", - "blake2", + "blake2 0.10.6", "bounded-collections", "bs58 0.5.0", "criterion 0.4.0", @@ -17029,11 +17183,22 @@ dependencies = [ "sp-std", ] +[[package]] +name = "sp-mixnet" +version = "0.1.0-dev" +dependencies = [ + "parity-scale-codec", + "scale-info", + "sp-api", + "sp-application-crypto", + "sp-std", +] + [[package]] name = "sp-mmr-primitives" version = "4.0.0-dev" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "ckb-merkle-mountain-range", "log", "parity-scale-codec", @@ -17231,7 +17396,7 @@ dependencies = [ name = "sp-state-machine" version = "0.28.0" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "assert_matches", "hash-db", "log", @@ -17353,7 +17518,7 @@ name = "sp-trie" version = "22.0.0" dependencies = [ "ahash 0.8.3", - "array-bytes", + "array-bytes 6.1.0", "criterion 0.4.0", "hash-db", "hashbrown 0.13.2", @@ -17659,7 +17824,7 @@ dependencies = [ "md-5", "rand 0.8.5", "ring 0.16.20", - "subtle", + "subtle 2.4.1", "thiserror", "tokio", "url", @@ -17825,7 +17990,7 @@ dependencies = [ name = "substrate-test-client" version = "2.0.1" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "async-trait", "futures", "parity-scale-codec", @@ -17850,7 +18015,7 @@ dependencies = [ name = "substrate-test-runtime" version = "2.0.0" dependencies = [ - "array-bytes", + "array-bytes 6.1.0", "frame-executive", "frame-support", "frame-system", @@ -17964,6 +18129,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" + [[package]] name = "subtle" version = "2.4.1" @@ -19055,7 +19226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" dependencies = [ "generic-array 0.14.7", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -19065,7 +19236,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", - "subtle", + "subtle 2.4.1", ] [[package]] @@ -19814,7 +19985,7 @@ dependencies = [ "sha1", "sha2 0.10.7", "signature 1.6.4", - "subtle", + "subtle 2.4.1", "thiserror", "tokio", "webpki 0.21.4", @@ -19908,7 +20079,7 @@ dependencies = [ "rtcp", "rtp", "sha-1 0.9.8", - "subtle", + "subtle 2.4.1", "thiserror", "tokio", "webrtc-util", diff --git a/Cargo.toml b/Cargo.toml index aa0b93737f7ef..75da6681465d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -217,6 +217,7 @@ members = [ "substrate/client/keystore", "substrate/client/merkle-mountain-range", "substrate/client/merkle-mountain-range/rpc", + "substrate/client/mixnet", "substrate/client/network-gossip", "substrate/client/network", "substrate/client/network/bitswap", @@ -298,6 +299,7 @@ members = [ "substrate/frame/membership", "substrate/frame/merkle-mountain-range", "substrate/frame/message-queue", + "substrate/frame/mixnet", "substrate/frame/multisig", "substrate/frame/nft-fractionalization", "substrate/frame/nfts", @@ -394,6 +396,7 @@ members = [ "substrate/primitives/maybe-compressed-blob", "substrate/primitives/merkle-mountain-range", "substrate/primitives/metadata-ir", + "substrate/primitives/mixnet", "substrate/primitives/npos-elections", "substrate/primitives/npos-elections/fuzzer", "substrate/primitives/offchain", diff --git a/substrate/bin/node/cli/Cargo.toml b/substrate/bin/node/cli/Cargo.toml index 5ce4c73f98c4f..49dc39099be03 100644 --- a/substrate/bin/node/cli/Cargo.toml +++ b/substrate/bin/node/cli/Cargo.toml @@ -60,6 +60,7 @@ sp-keystore = { path = "../../../primitives/keystore" } sp-consensus = { path = "../../../primitives/consensus/common" } sp-transaction-storage-proof = { path = "../../../primitives/transaction-storage-proof" } sp-io = { path = "../../../primitives/io" } +sp-mixnet = { path = "../../../primitives/mixnet" } sp-statement-store = { path = "../../../primitives/statement-store" } # client dependencies @@ -82,6 +83,7 @@ sc-service = { path = "../../../client/service", default-features = false} sc-telemetry = { path = "../../../client/telemetry" } sc-executor = { path = "../../../client/executor" } sc-authority-discovery = { path = "../../../client/authority-discovery" } +sc-mixnet = { path = "../../../client/mixnet" } sc-sync-state-rpc = { path = "../../../client/sync-state-rpc" } sc-sysinfo = { path = "../../../client/sysinfo" } sc-storage-monitor = { path = "../../../client/storage-monitor" } diff --git a/substrate/bin/node/cli/benches/block_production.rs b/substrate/bin/node/cli/benches/block_production.rs index b877aa7350228..246de8f3e925d 100644 --- a/substrate/bin/node/cli/benches/block_production.rs +++ b/substrate/bin/node/cli/benches/block_production.rs @@ -100,7 +100,7 @@ fn new_node(tokio_handle: Handle) -> node_cli::service::NewFullBase { wasm_runtime_overrides: None, }; - node_cli::service::new_full_base(config, false, |_, _| ()) + node_cli::service::new_full_base(config, None, false, |_, _| ()) .expect("creating a full node doesn't fail") } diff --git a/substrate/bin/node/cli/benches/transaction_pool.rs b/substrate/bin/node/cli/benches/transaction_pool.rs index d21edc55bbac3..47f890574151d 100644 --- a/substrate/bin/node/cli/benches/transaction_pool.rs +++ b/substrate/bin/node/cli/benches/transaction_pool.rs @@ -96,7 +96,7 @@ fn new_node(tokio_handle: Handle) -> node_cli::service::NewFullBase { wasm_runtime_overrides: None, }; - node_cli::service::new_full_base(config, false, |_, _| ()).expect("Creates node") + node_cli::service::new_full_base(config, None, false, |_, _| ()).expect("Creates node") } fn create_accounts(num: usize) -> Vec<sr25519::Pair> { diff --git a/substrate/bin/node/cli/src/chain_spec.rs b/substrate/bin/node/cli/src/chain_spec.rs index 51beaad03688a..52b480925aa94 100644 --- a/substrate/bin/node/cli/src/chain_spec.rs +++ b/substrate/bin/node/cli/src/chain_spec.rs @@ -33,6 +33,7 @@ use serde::{Deserialize, Serialize}; use sp_authority_discovery::AuthorityId as AuthorityDiscoveryId; use sp_consensus_babe::AuthorityId as BabeId; use sp_core::{crypto::UncheckedInto, sr25519, Pair, Public}; +use sp_mixnet::types::AuthorityId as MixnetId; use sp_runtime::{ traits::{IdentifyAccount, Verify}, Perbill, @@ -72,8 +73,9 @@ fn session_keys( babe: BabeId, im_online: ImOnlineId, authority_discovery: AuthorityDiscoveryId, + mixnet: MixnetId, ) -> SessionKeys { - SessionKeys { grandpa, babe, im_online, authority_discovery } + SessionKeys { grandpa, babe, im_online, authority_discovery, mixnet } } fn staging_testnet_config_genesis() -> RuntimeGenesisConfig { @@ -93,6 +95,7 @@ fn staging_testnet_config_genesis() -> RuntimeGenesisConfig { BabeId, ImOnlineId, AuthorityDiscoveryId, + MixnetId, )> = vec![ ( // 5Fbsd6WXDGiLTxunqeK5BATNiocfCqu9bS1yArVjCgeBLkVy @@ -111,6 +114,9 @@ fn staging_testnet_config_genesis() -> RuntimeGenesisConfig { // 5EZaeQ8djPcq9pheJUhgerXQZt9YaHnMJpiHMRhwQeinqUW8 array_bytes::hex2array_unchecked("6e7e4eb42cbd2e0ab4cae8708ce5509580b8c04d11f6758dbf686d50fe9f9106") .unchecked_into(), + // 5EZaeQ8djPcq9pheJUhgerXQZt9YaHnMJpiHMRhwQeinqUW8 + array_bytes::hex2array_unchecked("6e7e4eb42cbd2e0ab4cae8708ce5509580b8c04d11f6758dbf686d50fe9f9106") + .unchecked_into(), ), ( // 5ERawXCzCWkjVq3xz1W5KGNtVx2VdefvZ62Bw1FEuZW4Vny2 @@ -129,6 +135,9 @@ fn staging_testnet_config_genesis() -> RuntimeGenesisConfig { // 5DhLtiaQd1L1LU9jaNeeu9HJkP6eyg3BwXA7iNMzKm7qqruQ array_bytes::hex2array_unchecked("482dbd7297a39fa145c570552249c2ca9dd47e281f0c500c971b59c9dcdcd82e") .unchecked_into(), + // 5DhLtiaQd1L1LU9jaNeeu9HJkP6eyg3BwXA7iNMzKm7qqruQ + array_bytes::hex2array_unchecked("482dbd7297a39fa145c570552249c2ca9dd47e281f0c500c971b59c9dcdcd82e") + .unchecked_into(), ), ( // 5DyVtKWPidondEu8iHZgi6Ffv9yrJJ1NDNLom3X9cTDi98qp @@ -147,6 +156,9 @@ fn staging_testnet_config_genesis() -> RuntimeGenesisConfig { // 5DhKqkHRkndJu8vq7pi2Q5S3DfftWJHGxbEUNH43b46qNspH array_bytes::hex2array_unchecked("482a3389a6cf42d8ed83888cfd920fec738ea30f97e44699ada7323f08c3380a") .unchecked_into(), + // 5DhKqkHRkndJu8vq7pi2Q5S3DfftWJHGxbEUNH43b46qNspH + array_bytes::hex2array_unchecked("482a3389a6cf42d8ed83888cfd920fec738ea30f97e44699ada7323f08c3380a") + .unchecked_into(), ), ( // 5HYZnKWe5FVZQ33ZRJK1rG3WaLMztxWrrNDb1JRwaHHVWyP9 @@ -165,6 +177,9 @@ fn staging_testnet_config_genesis() -> RuntimeGenesisConfig { // 5C4vDQxA8LTck2xJEy4Yg1hM9qjDt4LvTQaMo4Y8ne43aU6x array_bytes::hex2array_unchecked("00299981a2b92f878baaf5dbeba5c18d4e70f2a1fcd9c61b32ea18daf38f4378") .unchecked_into(), + // 5C4vDQxA8LTck2xJEy4Yg1hM9qjDt4LvTQaMo4Y8ne43aU6x + array_bytes::hex2array_unchecked("00299981a2b92f878baaf5dbeba5c18d4e70f2a1fcd9c61b32ea18daf38f4378") + .unchecked_into(), ), ]; @@ -217,7 +232,7 @@ where /// Helper function to generate stash, controller and session key from seed. pub fn authority_keys_from_seed( seed: &str, -) -> (AccountId, AccountId, GrandpaId, BabeId, ImOnlineId, AuthorityDiscoveryId) { +) -> (AccountId, AccountId, GrandpaId, BabeId, ImOnlineId, AuthorityDiscoveryId, MixnetId) { ( get_account_id_from_seed::<sr25519::Public>(&format!("{}//stash", seed)), get_account_id_from_seed::<sr25519::Public>(seed), @@ -225,6 +240,7 @@ pub fn authority_keys_from_seed( get_from_seed::<BabeId>(seed), get_from_seed::<ImOnlineId>(seed), get_from_seed::<AuthorityDiscoveryId>(seed), + get_from_seed::<MixnetId>(seed), ) } @@ -237,6 +253,7 @@ pub fn testnet_genesis( BabeId, ImOnlineId, AuthorityDiscoveryId, + MixnetId, )>, initial_nominators: Vec<AccountId>, root_key: AccountId, @@ -306,7 +323,13 @@ pub fn testnet_genesis( ( x.0.clone(), x.0.clone(), - session_keys(x.2.clone(), x.3.clone(), x.4.clone(), x.5.clone()), + session_keys( + x.2.clone(), + x.3.clone(), + x.4.clone(), + x.5.clone(), + x.6.clone(), + ), ) }) .collect::<Vec<_>>(), @@ -367,6 +390,7 @@ pub fn testnet_genesis( ..Default::default() }, glutton: Default::default(), + mixnet: Default::default(), } } @@ -475,7 +499,7 @@ pub(crate) mod tests { sc_service_test::connectivity(integration_test_config_with_two_authorities(), |config| { let NewFullBase { task_manager, client, network, sync, transaction_pool, .. } = - new_full_base(config, false, |_, _| ())?; + new_full_base(config, None, false, |_, _| ())?; Ok(sc_service_test::TestNetComponents::new( task_manager, client, diff --git a/substrate/bin/node/cli/src/cli.rs b/substrate/bin/node/cli/src/cli.rs index 4e0d6303870cb..f3c0435fd32dc 100644 --- a/substrate/bin/node/cli/src/cli.rs +++ b/substrate/bin/node/cli/src/cli.rs @@ -27,6 +27,10 @@ pub struct Cli { #[clap(flatten)] pub run: sc_cli::RunCmd, + #[allow(missing_docs)] + #[clap(flatten)] + pub mixnet_params: sc_cli::MixnetParams, + /// Disable automatic hardware benchmarks. /// /// By default these benchmarks are automatically ran at startup and measure diff --git a/substrate/bin/node/cli/src/command.rs b/substrate/bin/node/cli/src/command.rs index 6bd8b76581acf..16d0415ff2637 100644 --- a/substrate/bin/node/cli/src/command.rs +++ b/substrate/bin/node/cli/src/command.rs @@ -111,7 +111,7 @@ pub fn run() -> Result<()> { }, BenchmarkCmd::Block(cmd) => { // ensure that we keep the task manager alive - let partial = new_partial(&config)?; + let partial = new_partial(&config, None)?; cmd.run(partial.client) }, #[cfg(not(feature = "runtime-benchmarks"))] @@ -122,7 +122,7 @@ pub fn run() -> Result<()> { #[cfg(feature = "runtime-benchmarks")] BenchmarkCmd::Storage(cmd) => { // ensure that we keep the task manager alive - let partial = new_partial(&config)?; + let partial = new_partial(&config, None)?; let db = partial.backend.expose_db(); let storage = partial.backend.expose_storage(); @@ -130,7 +130,7 @@ pub fn run() -> Result<()> { }, BenchmarkCmd::Overhead(cmd) => { // ensure that we keep the task manager alive - let partial = new_partial(&config)?; + let partial = new_partial(&config, None)?; let ext_builder = RemarkBuilder::new(partial.client.clone()); cmd.run( @@ -143,7 +143,7 @@ pub fn run() -> Result<()> { }, BenchmarkCmd::Extrinsic(cmd) => { // ensure that we keep the task manager alive - let partial = service::new_partial(&config)?; + let partial = service::new_partial(&config, None)?; // Register the *Remark* and *TKA* builders. let ext_factory = ExtrinsicFactory(vec![ Box::new(RemarkBuilder::new(partial.client.clone())), @@ -178,21 +178,21 @@ pub fn run() -> Result<()> { let runner = cli.create_runner(cmd)?; runner.async_run(|config| { let PartialComponents { client, task_manager, import_queue, .. } = - new_partial(&config)?; + new_partial(&config, None)?; Ok((cmd.run(client, import_queue), task_manager)) }) }, Some(Subcommand::ExportBlocks(cmd)) => { let runner = cli.create_runner(cmd)?; runner.async_run(|config| { - let PartialComponents { client, task_manager, .. } = new_partial(&config)?; + let PartialComponents { client, task_manager, .. } = new_partial(&config, None)?; Ok((cmd.run(client, config.database), task_manager)) }) }, Some(Subcommand::ExportState(cmd)) => { let runner = cli.create_runner(cmd)?; runner.async_run(|config| { - let PartialComponents { client, task_manager, .. } = new_partial(&config)?; + let PartialComponents { client, task_manager, .. } = new_partial(&config, None)?; Ok((cmd.run(client, config.chain_spec), task_manager)) }) }, @@ -200,7 +200,7 @@ pub fn run() -> Result<()> { let runner = cli.create_runner(cmd)?; runner.async_run(|config| { let PartialComponents { client, task_manager, import_queue, .. } = - new_partial(&config)?; + new_partial(&config, None)?; Ok((cmd.run(client, import_queue), task_manager)) }) }, @@ -211,7 +211,8 @@ pub fn run() -> Result<()> { Some(Subcommand::Revert(cmd)) => { let runner = cli.create_runner(cmd)?; runner.async_run(|config| { - let PartialComponents { client, task_manager, backend, .. } = new_partial(&config)?; + let PartialComponents { client, task_manager, backend, .. } = + new_partial(&config, None)?; let aux_revert = Box::new(|client: Arc<FullClient>, backend, blocks| { sc_consensus_babe::revert(client.clone(), backend, blocks)?; grandpa::revert(client, blocks)?; diff --git a/substrate/bin/node/cli/src/service.rs b/substrate/bin/node/cli/src/service.rs index 977c90e73e9ff..5a85f4cde0ae0 100644 --- a/substrate/bin/node/cli/src/service.rs +++ b/substrate/bin/node/cli/src/service.rs @@ -134,6 +134,7 @@ pub fn create_extrinsic( /// Creates a new partial node. pub fn new_partial( config: &Configuration, + mixnet_config: Option<&sc_mixnet::Config>, ) -> Result< sc_service::PartialComponents< FullClient, @@ -154,6 +155,7 @@ pub fn new_partial( grandpa::SharedVoterState, Option<Telemetry>, Arc<StatementStore>, + Option<sc_mixnet::ApiBackend>, ), >, ServiceError, @@ -246,6 +248,8 @@ pub fn new_partial( ) .map_err(|e| ServiceError::Other(format!("Statement store error: {:?}", e)))?; + let (mixnet_api, mixnet_api_backend) = mixnet_config.map(sc_mixnet::Api::new).unzip(); + let (rpc_extensions_builder, rpc_setup) = { let (_, grandpa_link, _) = &import_setup; @@ -287,6 +291,7 @@ pub fn new_partial( }, statement_store: rpc_statement_store.clone(), backend: rpc_backend.clone(), + mixnet_api: mixnet_api.as_ref().cloned(), }; node_rpc::create_full(deps).map_err(Into::into) @@ -303,7 +308,14 @@ pub fn new_partial( select_chain, import_queue, transaction_pool, - other: (rpc_extensions_builder, import_setup, rpc_setup, telemetry, statement_store), + other: ( + rpc_extensions_builder, + import_setup, + rpc_setup, + telemetry, + statement_store, + mixnet_api_backend, + ), }) } @@ -326,6 +338,7 @@ pub struct NewFullBase { /// Creates a full service from the configuration. pub fn new_full_base( config: Configuration, + mixnet_config: Option<sc_mixnet::Config>, disable_hardware_benchmarks: bool, with_startup_data: impl FnOnce( &sc_consensus_babe::BabeBlockImport<Block, FullClient, FullGrandpaBlockImport>, @@ -347,31 +360,36 @@ pub fn new_full_base( keystore_container, select_chain, transaction_pool, - other: (rpc_builder, import_setup, rpc_setup, mut telemetry, statement_store), - } = new_partial(&config)?; + other: + (rpc_builder, import_setup, rpc_setup, mut telemetry, statement_store, mixnet_api_backend), + } = new_partial(&config, mixnet_config.as_ref())?; let shared_voter_state = rpc_setup; let auth_disc_publish_non_global_ips = config.network.allow_non_globals_in_dht; let mut net_config = sc_network::config::FullNetworkConfiguration::new(&config.network); - let grandpa_protocol_name = grandpa::protocol_standard_name( - &client.block_hash(0).ok().flatten().expect("Genesis block exists; qed"), - &config.chain_spec, - ); + let genesis_hash = client.block_hash(0).ok().flatten().expect("Genesis block exists; qed"); + + let grandpa_protocol_name = grandpa::protocol_standard_name(&genesis_hash, &config.chain_spec); net_config.add_notification_protocol(grandpa::grandpa_peers_set_config( grandpa_protocol_name.clone(), )); let statement_handler_proto = sc_network_statement::StatementHandlerPrototype::new( - client - .block_hash(0u32.into()) - .ok() - .flatten() - .expect("Genesis block exists; qed"), + genesis_hash, config.chain_spec.fork_id(), ); net_config.add_notification_protocol(statement_handler_proto.set_config()); + let mixnet_protocol_name = + sc_mixnet::protocol_name(genesis_hash.as_ref(), config.chain_spec.fork_id()); + if let Some(mixnet_config) = &mixnet_config { + net_config.add_notification_protocol(sc_mixnet::peers_set_config( + mixnet_protocol_name.clone(), + mixnet_config, + )); + } + let warp_sync = Arc::new(grandpa::warp_proof::NetworkProvider::new( backend.clone(), import_setup.1.shared_authority_set().clone(), @@ -391,6 +409,20 @@ pub fn new_full_base( block_relay: None, })?; + if let Some(mixnet_config) = mixnet_config { + let mixnet = sc_mixnet::run( + mixnet_config, + mixnet_api_backend.expect("Mixnet API backend created if mixnet enabled"), + client.clone(), + sync_service.clone(), + network.clone(), + mixnet_protocol_name, + transaction_pool.clone(), + Some(keystore_container.keystore()), + ); + task_manager.spawn_handle().spawn("mixnet", None, mixnet); + } + let role = config.role.clone(); let force_authoring = config.force_authoring; let backoff_authoring_blocks = @@ -546,7 +578,7 @@ pub fn new_full_base( // and vote data availability than the observer. The observer has not // been tested extensively yet and having most nodes in a network run it // could lead to finality stalls. - let grandpa_config = grandpa::GrandpaParams { + let grandpa_params = grandpa::GrandpaParams { config: grandpa_config, link: grandpa_link, network: network.clone(), @@ -563,7 +595,7 @@ pub fn new_full_base( task_manager.spawn_essential_handle().spawn_blocking( "grandpa-voter", None, - grandpa::run_grandpa_voter(grandpa_config)?, + grandpa::run_grandpa_voter(grandpa_params)?, ); } @@ -623,8 +655,9 @@ pub fn new_full_base( /// Builds a new service for a full client. pub fn new_full(config: Configuration, cli: Cli) -> Result<TaskManager, ServiceError> { + let mixnet_config = cli.mixnet_params.config(config.role.is_authority()); let database_source = config.database.clone(); - let task_manager = new_full_base(config, cli.no_hardware_benchmarks, |_, _| ()) + let task_manager = new_full_base(config, mixnet_config, cli.no_hardware_benchmarks, |_, _| ()) .map(|NewFullBase { task_manager, .. }| task_manager)?; sc_storage_monitor::StorageMonitorService::try_spawn( @@ -702,6 +735,7 @@ mod tests { let NewFullBase { task_manager, client, network, sync, transaction_pool, .. } = new_full_base( config, + None, false, |block_import: &sc_consensus_babe::BabeBlockImport<Block, _, _>, babe_link: &sc_consensus_babe::BabeLink<Block>| { @@ -876,7 +910,7 @@ mod tests { crate::chain_spec::tests::integration_test_config_with_two_authorities(), |config| { let NewFullBase { task_manager, client, network, sync, transaction_pool, .. } = - new_full_base(config, false, |_, _| ())?; + new_full_base(config, None, false, |_, _| ())?; Ok(sc_service_test::TestNetComponents::new( task_manager, client, diff --git a/substrate/bin/node/rpc/Cargo.toml b/substrate/bin/node/rpc/Cargo.toml index ec8d16bd27ded..43db4ab9d34f7 100644 --- a/substrate/bin/node/rpc/Cargo.toml +++ b/substrate/bin/node/rpc/Cargo.toml @@ -23,6 +23,7 @@ sc-consensus-babe = { path = "../../../client/consensus/babe" } sc-consensus-babe-rpc = { path = "../../../client/consensus/babe/rpc" } sc-consensus-grandpa = { path = "../../../client/consensus/grandpa" } sc-consensus-grandpa-rpc = { path = "../../../client/consensus/grandpa/rpc" } +sc-mixnet = { path = "../../../client/mixnet" } sc-rpc = { path = "../../../client/rpc" } sc-rpc-api = { path = "../../../client/rpc-api" } sc-rpc-spec-v2 = { path = "../../../client/rpc-spec-v2" } diff --git a/substrate/bin/node/rpc/src/lib.rs b/substrate/bin/node/rpc/src/lib.rs index 6d8aa5ff0a9da..acc58777e912d 100644 --- a/substrate/bin/node/rpc/src/lib.rs +++ b/substrate/bin/node/rpc/src/lib.rs @@ -92,6 +92,8 @@ pub struct FullDeps<C, P, SC, B> { pub statement_store: Arc<dyn sp_statement_store::StatementStore>, /// The backend used by the node. pub backend: Arc<B>, + /// Mixnet API. + pub mixnet_api: Option<sc_mixnet::Api>, } /// Instantiate all Full RPC extensions. @@ -106,6 +108,7 @@ pub fn create_full<C, P, SC, B>( grandpa, statement_store, backend, + mixnet_api, }: FullDeps<C, P, SC, B>, ) -> Result<RpcModule<()>, Box<dyn std::error::Error + Send + Sync>> where @@ -133,6 +136,7 @@ where use sc_consensus_grandpa_rpc::{Grandpa, GrandpaApiServer}; use sc_rpc::{ dev::{Dev, DevApiServer}, + mixnet::MixnetApiServer, statement::StatementApiServer, }; use sc_rpc_spec_v2::chain_spec::{ChainSpec, ChainSpecApiServer}; @@ -196,5 +200,10 @@ where sc_rpc::statement::StatementStore::new(statement_store, deny_unsafe).into_rpc(); io.merge(statement_store)?; + if let Some(mixnet_api) = mixnet_api { + let mixnet = sc_rpc::mixnet::Mixnet::new(mixnet_api).into_rpc(); + io.merge(mixnet)?; + } + Ok(io) } diff --git a/substrate/bin/node/runtime/Cargo.toml b/substrate/bin/node/runtime/Cargo.toml index 7771b5f209719..e5bade12029e5 100644 --- a/substrate/bin/node/runtime/Cargo.toml +++ b/substrate/bin/node/runtime/Cargo.toml @@ -35,6 +35,7 @@ sp-block-builder = { path = "../../../primitives/block-builder", default-feature sp-genesis-builder = { version = "0.1.0-dev", default-features = false, path = "../../../primitives/genesis-builder" } sp-inherents = { path = "../../../primitives/inherents", default-features = false} node-primitives = { path = "../primitives", default-features = false} +sp-mixnet = { path = "../../../primitives/mixnet", default-features = false } sp-offchain = { path = "../../../primitives/offchain", default-features = false} sp-core = { path = "../../../primitives/core", default-features = false} sp-std = { path = "../../../primitives/std", default-features = false} @@ -88,6 +89,7 @@ pallet-identity = { path = "../../../frame/identity", default-features = false} pallet-lottery = { path = "../../../frame/lottery", default-features = false} pallet-membership = { path = "../../../frame/membership", default-features = false} pallet-message-queue = { path = "../../../frame/message-queue", default-features = false} +pallet-mixnet = { path = "../../../frame/mixnet", default-features = false } pallet-mmr = { path = "../../../frame/merkle-mountain-range", default-features = false} pallet-multisig = { path = "../../../frame/multisig", default-features = false} pallet-nfts = { path = "../../../frame/nfts", default-features = false} @@ -185,6 +187,7 @@ std = [ "pallet-lottery/std", "pallet-membership/std", "pallet-message-queue/std", + "pallet-mixnet/std", "pallet-mmr/std", "pallet-multisig/std", "pallet-nft-fractionalization/std", @@ -235,6 +238,7 @@ std = [ "sp-genesis-builder/std", "sp-inherents/std", "sp-io/std", + "sp-mixnet/std", "sp-offchain/std", "sp-runtime/std", "sp-session/std", @@ -281,6 +285,7 @@ runtime-benchmarks = [ "pallet-lottery/runtime-benchmarks", "pallet-membership/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", + "pallet-mixnet/runtime-benchmarks", "pallet-mmr/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", "pallet-nft-fractionalization/runtime-benchmarks", @@ -354,6 +359,7 @@ try-runtime = [ "pallet-lottery/try-runtime", "pallet-membership/try-runtime", "pallet-message-queue/try-runtime", + "pallet-mixnet/try-runtime", "pallet-mmr/try-runtime", "pallet-multisig/try-runtime", "pallet-nft-fractionalization/try-runtime", diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 9e3b8153e2b5c..2070e3f12d04f 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -589,6 +589,7 @@ impl_opaque_keys! { pub babe: Babe, pub im_online: ImOnline, pub authority_discovery: AuthorityDiscovery, + pub mixnet: Mixnet, } } @@ -1048,7 +1049,7 @@ impl pallet_democracy::Config for Runtime { pallet_collective::EnsureProportionAtLeast<AccountId, TechnicalCollective, 2, 3>; type InstantOrigin = pallet_collective::EnsureProportionAtLeast<AccountId, TechnicalCollective, 1, 1>; - type InstantAllowed = frame_support::traits::ConstBool<true>; + type InstantAllowed = ConstBool<true>; type FastTrackVotingPeriod = FastTrackVotingPeriod; // To cancel a proposal which has been passed, 2/3 of the council must agree to it. type CancellationOrigin = @@ -2028,6 +2029,29 @@ impl pallet_broker::Config for Runtime { type PriceAdapter = pallet_broker::Linear; } +parameter_types! { + pub const MixnetNumCoverToCurrentBlocks: BlockNumber = 3; + pub const MixnetNumRequestsToCurrentBlocks: BlockNumber = 3; + pub const MixnetNumCoverToPrevBlocks: BlockNumber = 3; + pub const MixnetNumRegisterStartSlackBlocks: BlockNumber = 3; + pub const MixnetNumRegisterEndSlackBlocks: BlockNumber = 3; + pub const MixnetRegistrationPriority: TransactionPriority = ImOnlineUnsignedPriority::get() - 1; +} + +impl pallet_mixnet::Config for Runtime { + type MaxAuthorities = MaxAuthorities; + type MaxExternalAddressSize = ConstU32<128>; + type MaxExternalAddressesPerMixnode = ConstU32<16>; + type NextSessionRotation = Babe; + type NumCoverToCurrentBlocks = MixnetNumCoverToCurrentBlocks; + type NumRequestsToCurrentBlocks = MixnetNumRequestsToCurrentBlocks; + type NumCoverToPrevBlocks = MixnetNumCoverToPrevBlocks; + type NumRegisterStartSlackBlocks = MixnetNumRegisterStartSlackBlocks; + type NumRegisterEndSlackBlocks = MixnetNumRegisterEndSlackBlocks; + type RegistrationPriority = MixnetRegistrationPriority; + type MinMixnodes = ConstU32<7>; // Low to allow small testing networks +} + construct_runtime!( pub struct Runtime { @@ -2104,6 +2128,7 @@ construct_runtime!( SafeMode: pallet_safe_mode, Statement: pallet_statement, Broker: pallet_broker, + Mixnet: pallet_mixnet, } ); @@ -2663,6 +2688,24 @@ impl_runtime_apis! { } } + impl sp_mixnet::runtime_api::MixnetApi<Block> for Runtime { + fn session_status() -> sp_mixnet::types::SessionStatus { + Mixnet::session_status() + } + + fn prev_mixnodes() -> Result<Vec<sp_mixnet::types::Mixnode>, sp_mixnet::types::MixnodesErr> { + Mixnet::prev_mixnodes() + } + + fn current_mixnodes() -> Result<Vec<sp_mixnet::types::Mixnode>, sp_mixnet::types::MixnodesErr> { + Mixnet::current_mixnodes() + } + + fn maybe_register(session_index: sp_mixnet::types::SessionIndex, mixnode: sp_mixnet::types::Mixnode) -> bool { + Mixnet::maybe_register(session_index, mixnode) + } + } + impl sp_session::SessionKeys<Block> for Runtime { fn generate_session_keys(seed: Option<Vec<u8>>) -> Vec<u8> { SessionKeys::generate(seed) diff --git a/substrate/bin/node/testing/src/genesis.rs b/substrate/bin/node/testing/src/genesis.rs index 6e7bcebfc00d1..ab5311751a55f 100644 --- a/substrate/bin/node/testing/src/genesis.rs +++ b/substrate/bin/node/testing/src/genesis.rs @@ -109,5 +109,6 @@ pub fn config_endowed(code: Option<&[u8]>, extra_endowed: Vec<AccountId>) -> Run trash_data_count: Default::default(), ..Default::default() }, + mixnet: Default::default(), } } diff --git a/substrate/bin/node/testing/src/keyring.rs b/substrate/bin/node/testing/src/keyring.rs index b4b714d9083d6..22a8f5deb19f7 100644 --- a/substrate/bin/node/testing/src/keyring.rs +++ b/substrate/bin/node/testing/src/keyring.rs @@ -64,6 +64,7 @@ pub fn to_session_keys( babe: sr25519_keyring.to_owned().public().into(), im_online: sr25519_keyring.to_owned().public().into(), authority_discovery: sr25519_keyring.to_owned().public().into(), + mixnet: sr25519_keyring.to_owned().public().into(), } } diff --git a/substrate/bin/utils/chain-spec-builder/src/lib.rs b/substrate/bin/utils/chain-spec-builder/src/lib.rs index 528b6b70115a0..2b88e40ef74fd 100644 --- a/substrate/bin/utils/chain-spec-builder/src/lib.rs +++ b/substrate/bin/utils/chain-spec-builder/src/lib.rs @@ -179,7 +179,7 @@ pub fn generate_authority_keys_and_store( .map_err(|err| err.to_string())? .into(); - let (_, _, grandpa, babe, im_online, authority_discovery) = + let (_, _, grandpa, babe, im_online, authority_discovery, mixnet) = chain_spec::authority_keys_from_seed(seed); let insert_key = |key_type, public| { @@ -198,6 +198,8 @@ pub fn generate_authority_keys_and_store( sp_core::crypto::key_types::AUTHORITY_DISCOVERY, authority_discovery.as_slice(), )?; + + insert_key(sp_core::crypto::key_types::MIXNET, mixnet.as_slice())?; } Ok(()) diff --git a/substrate/client/cli/Cargo.toml b/substrate/client/cli/Cargo.toml index b78287be890fc..98928700328fc 100644 --- a/substrate/client/cli/Cargo.toml +++ b/substrate/client/cli/Cargo.toml @@ -33,6 +33,7 @@ tokio = { version = "1.22.0", features = ["signal", "rt-multi-thread", "parking_ sc-client-api = { path = "../api" } sc-client-db = { path = "../db", default-features = false} sc-keystore = { path = "../keystore" } +sc-mixnet = { path = "../mixnet" } sc-network = { path = "../network" } sc-service = { path = "../service", default-features = false} sc-telemetry = { path = "../telemetry" } diff --git a/substrate/client/cli/src/params/mixnet_params.rs b/substrate/client/cli/src/params/mixnet_params.rs new file mode 100644 index 0000000000000..4758a84ec458d --- /dev/null +++ b/substrate/client/cli/src/params/mixnet_params.rs @@ -0,0 +1,67 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use clap::Args; +use sp_core::H256; +use std::str::FromStr; + +fn parse_kx_secret(s: &str) -> Result<sc_mixnet::KxSecret, String> { + H256::from_str(s).map(H256::to_fixed_bytes).map_err(|err| err.to_string()) +} + +/// Parameters used to create the mixnet configuration. +#[derive(Debug, Clone, Args)] +pub struct MixnetParams { + /// Enable the mixnet service. + /// + /// This will make the mixnet RPC methods available. If the node is running as a validator, it + /// will also attempt to register and operate as a mixnode. + #[arg(long)] + pub mixnet: bool, + + /// The mixnet key-exchange secret to use in session 0. + /// + /// Should be 64 hex characters, giving a 32-byte secret. + /// + /// WARNING: Secrets provided as command-line arguments are easily exposed. Use of this option + /// should be limited to development and testing. + #[arg(long, value_name = "SECRET", value_parser = parse_kx_secret)] + pub mixnet_session_0_kx_secret: Option<sc_mixnet::KxSecret>, +} + +impl MixnetParams { + /// Returns the mixnet configuration, or `None` if the mixnet is disabled. + pub fn config(&self, is_authority: bool) -> Option<sc_mixnet::Config> { + self.mixnet.then(|| { + let mut config = sc_mixnet::Config { + core: sc_mixnet::CoreConfig { + session_0_kx_secret: self.mixnet_session_0_kx_secret, + ..Default::default() + }, + ..Default::default() + }; + if !is_authority { + // Only authorities can be mixnodes; don't attempt to register + config.substrate.register = false; + // Only mixnodes need to allow connections from non-mixnodes + config.substrate.num_gateway_slots = 0; + } + config + }) + } +} diff --git a/substrate/client/cli/src/params/mod.rs b/substrate/client/cli/src/params/mod.rs index a73bd8844fec4..f07223ec6a73e 100644 --- a/substrate/client/cli/src/params/mod.rs +++ b/substrate/client/cli/src/params/mod.rs @@ -19,6 +19,7 @@ mod database_params; mod import_params; mod keystore_params; mod message_params; +mod mixnet_params; mod network_params; mod node_key_params; mod offchain_worker_params; @@ -39,9 +40,10 @@ use sp_runtime::{ use std::{fmt::Debug, str::FromStr}; pub use crate::params::{ - database_params::*, import_params::*, keystore_params::*, message_params::*, network_params::*, - node_key_params::*, offchain_worker_params::*, prometheus_params::*, pruning_params::*, - runtime_params::*, shared_params::*, telemetry_params::*, transaction_pool_params::*, + database_params::*, import_params::*, keystore_params::*, message_params::*, mixnet_params::*, + network_params::*, node_key_params::*, offchain_worker_params::*, prometheus_params::*, + pruning_params::*, runtime_params::*, shared_params::*, telemetry_params::*, + transaction_pool_params::*, }; /// Parse Ss58AddressFormat diff --git a/substrate/client/mixnet/Cargo.toml b/substrate/client/mixnet/Cargo.toml new file mode 100644 index 0000000000000..86c5a37754afb --- /dev/null +++ b/substrate/client/mixnet/Cargo.toml @@ -0,0 +1,36 @@ +[package] +description = "Substrate mixnet service" +name = "sc-mixnet" +version = "0.1.0-dev" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" +authors = ["Parity Technologies <admin@parity.io>"] +edition = "2021" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +array-bytes = "4.1" +arrayvec = "0.7.2" +blake2 = "0.10.4" +codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] } +futures = "0.3.25" +futures-timer = "3.0.2" +libp2p-identity = { version = "0.1.3", features = ["peerid"] } +log = "0.4.17" +mixnet = "0.7.0" +multiaddr = "0.17.1" +parking_lot = "0.12.1" +sc-client-api = { path = "../api" } +sc-network = { path = "../network" } +sc-transaction-pool-api = { path = "../transaction-pool/api" } +sp-api = { path = "../../primitives/api" } +sp-consensus = { path = "../../primitives/consensus/common" } +sp-core = { path = "../../primitives/core" } +sp-keystore = { path = "../../primitives/keystore" } +sp-mixnet = { path = "../../primitives/mixnet" } +sp-runtime = { path = "../../primitives/runtime" } +thiserror = "1.0" diff --git a/substrate/client/mixnet/README.md b/substrate/client/mixnet/README.md new file mode 100644 index 0000000000000..cd8d147408387 --- /dev/null +++ b/substrate/client/mixnet/README.md @@ -0,0 +1,3 @@ +Substrate mixnet service. + +License: GPL-3.0-or-later WITH Classpath-exception-2.0 diff --git a/substrate/client/mixnet/src/api.rs b/substrate/client/mixnet/src/api.rs new file mode 100644 index 0000000000000..42a2e395345dc --- /dev/null +++ b/substrate/client/mixnet/src/api.rs @@ -0,0 +1,69 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use super::{config::Config, error::Error, request::Request}; +use futures::{ + channel::{mpsc, oneshot}, + SinkExt, +}; +use sp_core::Bytes; +use std::future::Future; + +/// The other end of an [`Api`]. This should be passed to [`run`](super::run::run). +pub struct ApiBackend { + pub(super) request_receiver: mpsc::Receiver<Request>, +} + +/// Interface to the mixnet service. +#[derive(Clone)] +pub struct Api { + request_sender: mpsc::Sender<Request>, +} + +impl Api { + /// Create a new `Api`. The [`ApiBackend`] should be passed to [`run`](super::run::run). + pub fn new(config: &Config) -> (Self, ApiBackend) { + let (request_sender, request_receiver) = mpsc::channel(config.substrate.request_buffer); + (Self { request_sender }, ApiBackend { request_receiver }) + } + + /// Submit an extrinsic via the mixnet. + /// + /// Returns a [`Future`] which returns another `Future`. + /// + /// The first `Future` resolves as soon as there is space in the mixnet service queue. The + /// second `Future` resolves once a reply is received over the mixnet (or sooner if there is an + /// error). + /// + /// The first `Future` references `self`, but the second does not. This makes it possible to + /// submit concurrent mixnet requests using a single `Api` instance. + pub async fn submit_extrinsic( + &mut self, + extrinsic: Bytes, + ) -> impl Future<Output = Result<(), Error>> { + let (reply_sender, reply_receiver) = oneshot::channel(); + let res = self + .request_sender + .feed(Request::SubmitExtrinsic { extrinsic, reply_sender }) + .await; + async move { + res.map_err(|_| Error::ServiceUnavailable)?; + reply_receiver.await.map_err(|_| Error::ServiceUnavailable)? + } + } +} diff --git a/substrate/client/mixnet/src/config.rs b/substrate/client/mixnet/src/config.rs new file mode 100644 index 0000000000000..b716237ab7b53 --- /dev/null +++ b/substrate/client/mixnet/src/config.rs @@ -0,0 +1,88 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +pub use mixnet::core::Config as CoreConfig; +use std::time::Duration; + +/// Substrate-specific mixnet configuration. +#[derive(Clone, Debug)] +pub struct SubstrateConfig { + /// Attempt to register the local node as a mixnode? + pub register: bool, + /// Maximum number of incoming mixnet connections to accept from non-mixnodes. If the local + /// node will never be a mixnode, this can be set to 0. + pub num_gateway_slots: u32, + + /// Number of requests to the mixnet service that can be buffered, in addition to the one per + /// [`Api`](super::api::Api) instance. Note that this does not include requests that are being + /// actively handled. + pub request_buffer: usize, + /// Used to determine the number of SURBs to include in request messages: the maximum number of + /// SURBs needed for a single reply is multiplied by this. This should not be set to 0. + pub surb_factor: usize, + + /// Maximum number of submit extrinsic requests waiting for their delay to elapse. When at the + /// limit, any submit extrinsic requests that arrive will simply be dropped. + pub extrinsic_queue_capacity: usize, + /// Mean delay between receiving a submit extrinsic request and actually submitting the + /// extrinsic. This should really be the same for all nodes! + pub mean_extrinsic_delay: Duration, + /// Maximum number of extrinsics being actively submitted. If a submit extrinsic request's + /// delay elapses and we are already at this limit, the request will simply be dropped. + pub max_pending_extrinsics: usize, +} + +impl Default for SubstrateConfig { + fn default() -> Self { + Self { + register: true, + num_gateway_slots: 150, + + request_buffer: 4, + surb_factor: 2, + + extrinsic_queue_capacity: 50, + mean_extrinsic_delay: Duration::from_secs(1), + max_pending_extrinsics: 20, + } + } +} + +/// Mixnet configuration. +#[derive(Clone, Debug)] +pub struct Config { + /// Core configuration. + pub core: CoreConfig, + /// Request manager configuration. + pub request_manager: mixnet::request_manager::Config, + /// Reply manager configuration. + pub reply_manager: mixnet::reply_manager::Config, + /// Substrate-specific configuration. + pub substrate: SubstrateConfig, +} + +impl Default for Config { + fn default() -> Self { + Self { + core: Default::default(), + request_manager: Default::default(), + reply_manager: Default::default(), + substrate: Default::default(), + } + } +} diff --git a/substrate/client/mixnet/src/error.rs b/substrate/client/mixnet/src/error.rs new file mode 100644 index 0000000000000..88942dbe3b36f --- /dev/null +++ b/substrate/client/mixnet/src/error.rs @@ -0,0 +1,56 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use codec::{Decode, Encode}; +use mixnet::core::PostErr; + +/// Error handling a request. Sent in replies over the mixnet. +#[derive(Debug, thiserror::Error, Decode, Encode)] +pub enum RemoteErr { + /// An error that doesn't map to any of the other variants. + #[error("{0}")] + Other(String), + /// Failed to decode the request. + #[error("Failed to decode the request: {0}")] + Decode(String), +} + +/// Mixnet error. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Failed to communicate with the mixnet service. Possibly it panicked. The node probably + /// needs to be restarted. + #[error( + "Failed to communicate with the mixnet service; the node probably needs to be restarted" + )] + ServiceUnavailable, + /// Did not receive a reply after the configured number of attempts. + #[error("Did not receive a reply from the mixnet after the configured number of attempts")] + NoReply, + /// Received a malformed reply. + #[error("Received a malformed reply from the mixnet")] + BadReply, + /// Failed to post the request to the mixnet. Note that some [`PostErr`] variants, eg + /// [`PostErr::NotEnoughSpaceInQueue`], are handled internally and will never be returned from + /// the top-level API. + #[error("Failed to post the request to the mixnet: {0}")] + Post(#[from] PostErr), + /// Error reported by destination mixnode. + #[error("Error reported by the destination mixnode: {0}")] + Remote(#[from] RemoteErr), +} diff --git a/substrate/client/mixnet/src/extrinsic_queue.rs b/substrate/client/mixnet/src/extrinsic_queue.rs new file mode 100644 index 0000000000000..b6f6f9ebae998 --- /dev/null +++ b/substrate/client/mixnet/src/extrinsic_queue.rs @@ -0,0 +1,94 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! [`ExtrinsicQueue`] is a queue for extrinsics received from the mixnet. These extrinsics are +//! explicitly delayed by a random amount, to decorrelate the times at which they are received from +//! the times at which they are broadcast to peers. + +use mixnet::reply_manager::ReplyContext; +use std::{cmp::Ordering, collections::BinaryHeap, time::Instant}; + +/// An extrinsic that should be submitted to the transaction pool after `deadline`. `Eq` and `Ord` +/// are implemented for this to support use in `BinaryHeap`s. Only `deadline` is compared. +struct DelayedExtrinsic<E> { + /// When the extrinsic should actually be submitted to the pool. + deadline: Instant, + extrinsic: E, + reply_context: ReplyContext, +} + +impl<E> PartialEq for DelayedExtrinsic<E> { + fn eq(&self, other: &Self) -> bool { + self.deadline == other.deadline + } +} + +impl<E> Eq for DelayedExtrinsic<E> {} + +impl<E> PartialOrd for DelayedExtrinsic<E> { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl<E> Ord for DelayedExtrinsic<E> { + fn cmp(&self, other: &Self) -> Ordering { + // Extrinsics with the earliest deadline considered greatest + self.deadline.cmp(&other.deadline).reverse() + } +} + +pub struct ExtrinsicQueue<E> { + capacity: usize, + queue: BinaryHeap<DelayedExtrinsic<E>>, + next_deadline_changed: bool, +} + +impl<E> ExtrinsicQueue<E> { + pub fn new(capacity: usize) -> Self { + Self { capacity, queue: BinaryHeap::with_capacity(capacity), next_deadline_changed: false } + } + + pub fn next_deadline(&self) -> Option<Instant> { + self.queue.peek().map(|extrinsic| extrinsic.deadline) + } + + pub fn next_deadline_changed(&mut self) -> bool { + let changed = self.next_deadline_changed; + self.next_deadline_changed = false; + changed + } + + pub fn has_space(&self) -> bool { + self.queue.len() < self.capacity + } + + pub fn insert(&mut self, deadline: Instant, extrinsic: E, reply_context: ReplyContext) { + debug_assert!(self.has_space()); + let prev_deadline = self.next_deadline(); + self.queue.push(DelayedExtrinsic { deadline, extrinsic, reply_context }); + if self.next_deadline() != prev_deadline { + self.next_deadline_changed = true; + } + } + + pub fn pop(&mut self) -> Option<(E, ReplyContext)> { + self.next_deadline_changed = true; + self.queue.pop().map(|extrinsic| (extrinsic.extrinsic, extrinsic.reply_context)) + } +} diff --git a/substrate/client/mixnet/src/lib.rs b/substrate/client/mixnet/src/lib.rs new file mode 100644 index 0000000000000..dfbb50dad6b49 --- /dev/null +++ b/substrate/client/mixnet/src/lib.rs @@ -0,0 +1,44 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Substrate mixnet service. This implements the [Substrate Mix Network +//! Specification](https://paritytech.github.io/mixnet-spec/). + +#![warn(missing_docs)] +#![forbid(unsafe_code)] + +mod api; +mod config; +mod error; +mod extrinsic_queue; +mod maybe_inf_delay; +mod packet_dispatcher; +mod peer_id; +mod protocol; +mod request; +mod run; +mod sync_with_runtime; + +pub use self::{ + api::{Api, ApiBackend}, + config::{Config, CoreConfig, SubstrateConfig}, + error::{Error, RemoteErr}, + protocol::{peers_set_config, protocol_name}, + run::run, +}; +pub use mixnet::core::{KxSecret, PostErr, TopologyErr}; diff --git a/substrate/client/mixnet/src/maybe_inf_delay.rs b/substrate/client/mixnet/src/maybe_inf_delay.rs new file mode 100644 index 0000000000000..feb0d038560a3 --- /dev/null +++ b/substrate/client/mixnet/src/maybe_inf_delay.rs @@ -0,0 +1,111 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use futures::{future::FusedFuture, FutureExt}; +use futures_timer::Delay; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll, Waker}, + time::Duration, +}; + +enum Inner { + Infinite { + /// Waker from the most recent `poll` call. If `None`, either `poll` has not been called + /// yet, we returned `Poll::Ready` from the last call, or the waker is attached to `delay`. + waker: Option<Waker>, + delay: Option<Delay>, + }, + Finite(Delay), +} + +/// Like [`Delay`] but the duration can be infinite (in which case the future will never fire). +/// Unlike [`Delay`], implements [`FusedFuture`], with [`is_terminated`](Self::is_terminated) +/// returning `true` when the delay is infinite. As with [`Delay`], once [`poll`](Self::poll) +/// returns [`Poll::Ready`], it will continue to do so until [`reset`](Self::reset) is called. +pub struct MaybeInfDelay(Inner); + +impl MaybeInfDelay { + /// Create a new `MaybeInfDelay` future. If `duration` is [`Some`], the future will fire after + /// the given duration has elapsed. If `duration` is [`None`], the future will "never" fire + /// (although see [`reset`](Self::reset)). + pub fn new(duration: Option<Duration>) -> Self { + match duration { + Some(duration) => Self(Inner::Finite(Delay::new(duration))), + None => Self(Inner::Infinite { waker: None, delay: None }), + } + } + + /// Reset the timer. `duration` is handled just like in [`new`](Self::new). Note that while + /// this is similar to `std::mem::replace(&mut self, MaybeInfDelay::new(duration))`, with + /// `replace` you would have to manually ensure [`poll`](Self::poll) was called again; with + /// `reset` this is not necessary. + pub fn reset(&mut self, duration: Option<Duration>) { + match duration { + Some(duration) => match &mut self.0 { + Inner::Infinite { waker, delay } => { + let mut delay = match delay.take() { + Some(mut delay) => { + delay.reset(duration); + delay + }, + None => Delay::new(duration), + }; + if let Some(waker) = waker.take() { + let mut cx = Context::from_waker(&waker); + match delay.poll_unpin(&mut cx) { + Poll::Pending => (), // Waker attached to delay + Poll::Ready(_) => waker.wake(), + } + } + self.0 = Inner::Finite(delay); + }, + Inner::Finite(delay) => delay.reset(duration), + }, + None => + self.0 = match std::mem::replace( + &mut self.0, + Inner::Infinite { waker: None, delay: None }, + ) { + Inner::Finite(delay) => Inner::Infinite { waker: None, delay: Some(delay) }, + infinite => infinite, + }, + } + } +} + +impl Future for MaybeInfDelay { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + match &mut self.0 { + Inner::Infinite { waker, .. } => { + *waker = Some(cx.waker().clone()); + Poll::Pending + }, + Inner::Finite(delay) => delay.poll_unpin(cx), + } + } +} + +impl FusedFuture for MaybeInfDelay { + fn is_terminated(&self) -> bool { + matches!(self.0, Inner::Infinite { .. }) + } +} diff --git a/substrate/client/mixnet/src/packet_dispatcher.rs b/substrate/client/mixnet/src/packet_dispatcher.rs new file mode 100644 index 0000000000000..856208ecb3426 --- /dev/null +++ b/substrate/client/mixnet/src/packet_dispatcher.rs @@ -0,0 +1,198 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! [`AddressedPacket`] dispatching. + +use super::peer_id::{from_core_peer_id, to_core_peer_id}; +use arrayvec::ArrayVec; +use libp2p_identity::PeerId; +use log::{debug, warn}; +use mixnet::core::{AddressedPacket, NetworkStatus, Packet, PeerId as CorePeerId}; +use parking_lot::Mutex; +use sc_network::{NetworkNotification, ProtocolName}; +use std::{collections::HashMap, future::Future, sync::Arc}; + +const LOG_TARGET: &str = "mixnet"; + +/// Packet queue for a peer. +/// +/// Ideally we would use `Rc<RefCell<_>>`, but that would prevent the top-level future from being +/// automatically marked `Send`. I believe it would be safe to manually mark it `Send`, but using +/// `Arc<Mutex<_>>` here is not really a big deal. +struct PeerQueue(Mutex<ArrayVec<Box<Packet>, 2>>); + +impl PeerQueue { + fn new() -> Self { + Self(Mutex::new(ArrayVec::new())) + } + + /// Push `packet` onto the queue. Returns `true` if the queue was previously empty. Fails if + /// the queue is full. + fn push(&self, packet: Box<Packet>) -> Result<bool, ()> { + let mut queue = self.0.lock(); + if queue.is_full() { + Err(()) + } else { + let was_empty = queue.is_empty(); + queue.push(packet); + Ok(was_empty) + } + } + + /// Drop all packets from the queue. + fn clear(&self) { + let mut queue = self.0.lock(); + queue.clear(); + } + + /// Pop the packet at the head of the queue and return it, or, if the queue is empty, return + /// `None`. Also returns `true` if there are more packets in the queue. + fn pop(&self) -> (Option<Box<Packet>>, bool) { + let mut queue = self.0.lock(); + let packet = queue.pop(); + (packet, !queue.is_empty()) + } +} + +/// A peer which has packets ready to send but is not currently being serviced. +pub struct ReadyPeer { + id: PeerId, + /// The peer's packet queue. Not empty. + queue: Arc<PeerQueue>, +} + +impl ReadyPeer { + /// If a future is returned, and if that future returns `Some`, this function should be called + /// again to send the next packet queued for the peer; `self` is placed in the `Some` to make + /// this straightforward. Otherwise, we have either sent or dropped all packets queued for the + /// peer, and it can be forgotten about for the time being. + pub fn send_packet( + self, + network: &impl NetworkNotification, + protocol_name: ProtocolName, + ) -> Option<impl Future<Output = Option<Self>>> { + match network.notification_sender(self.id, protocol_name) { + Err(err) => { + debug!( + target: LOG_TARGET, + "Failed to get notification sender for peer ID {}: {err}", self.id + ); + self.queue.clear(); + None + }, + Ok(sender) => Some(async move { + match sender.ready().await.and_then(|mut ready| { + let (packet, more_packets) = self.queue.pop(); + let packet = + packet.expect("Should only be called if there is a packet to send"); + ready.send((packet as Box<[_]>).into())?; + Ok(more_packets) + }) { + Err(err) => { + debug!( + target: LOG_TARGET, + "Notification sender for peer ID {} failed: {err}", self.id + ); + self.queue.clear(); + None + }, + Ok(more_packets) => more_packets.then(|| self), + } + }), + } + } +} + +pub struct PacketDispatcher { + /// Peer ID of the local node. Only used to implement [`NetworkStatus`]. + local_peer_id: CorePeerId, + /// Packet queue for each connected peer. These queues are very short and only exist to give + /// packets somewhere to sit while waiting for notification senders to be ready. + peer_queues: HashMap<CorePeerId, Arc<PeerQueue>>, +} + +impl PacketDispatcher { + pub fn new(local_peer_id: &CorePeerId) -> Self { + Self { local_peer_id: *local_peer_id, peer_queues: HashMap::new() } + } + + pub fn add_peer(&mut self, id: &PeerId) { + let Some(core_id) = to_core_peer_id(id) else { + debug!(target: LOG_TARGET, + "Cannot add peer; failed to convert libp2p peer ID {id} to mixnet peer ID"); + return + }; + if self.peer_queues.insert(core_id, Arc::new(PeerQueue::new())).is_some() { + warn!(target: LOG_TARGET, "Two stream opened notifications for peer ID {id}"); + } + } + + pub fn remove_peer(&mut self, id: &PeerId) { + let Some(core_id) = to_core_peer_id(id) else { + debug!(target: LOG_TARGET, + "Cannot remove peer; failed to convert libp2p peer ID {id} to mixnet peer ID"); + return + }; + if self.peer_queues.remove(&core_id).is_none() { + warn!(target: LOG_TARGET, "Stream closed notification for unknown peer ID {id}"); + } + } + + /// If the peer is not connected or the peer's packet queue is full, the packet is dropped. + /// Otherwise the packet is pushed onto the peer's queue, and if the queue was previously empty + /// a [`ReadyPeer`] is returned. + pub fn dispatch(&mut self, packet: AddressedPacket) -> Option<ReadyPeer> { + let Some(queue) = self.peer_queues.get_mut(&packet.peer_id) else { + debug!(target: LOG_TARGET, "Dropped packet to mixnet peer ID {:x?}; not connected", + packet.peer_id); + return None + }; + + match queue.push(packet.packet) { + Err(_) => { + debug!( + target: LOG_TARGET, + "Dropped packet to mixnet peer ID {:x?}; peer queue full", packet.peer_id + ); + None + }, + Ok(true) => { + // Queue was empty. Construct and return a ReadyPeer. + let Some(id) = from_core_peer_id(&packet.peer_id) else { + debug!(target: LOG_TARGET, "Cannot send packet; \ + failed to convert mixnet peer ID {:x?} to libp2p peer ID", + packet.peer_id); + queue.clear(); + return None + }; + Some(ReadyPeer { id, queue: queue.clone() }) + }, + Ok(false) => None, // Queue was not empty + } + } +} + +impl NetworkStatus for PacketDispatcher { + fn local_peer_id(&self) -> CorePeerId { + self.local_peer_id + } + + fn is_connected(&self, peer_id: &CorePeerId) -> bool { + self.peer_queues.contains_key(peer_id) + } +} diff --git a/substrate/client/mixnet/src/peer_id.rs b/substrate/client/mixnet/src/peer_id.rs new file mode 100644 index 0000000000000..7984da8c75be7 --- /dev/null +++ b/substrate/client/mixnet/src/peer_id.rs @@ -0,0 +1,44 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use libp2p_identity::PeerId; +use mixnet::core::PeerId as CorePeerId; + +/// Convert a libp2p [`PeerId`] into a mixnet core [`PeerId`](CorePeerId). +/// +/// This will succeed only if `peer_id` is an Ed25519 public key ("hashed" using the identity +/// hasher). Returns `None` on failure. +pub fn to_core_peer_id(peer_id: &PeerId) -> Option<CorePeerId> { + let hash = peer_id.as_ref(); + if hash.code() != 0 { + // Hash is not identity + return None + } + let public = libp2p_identity::PublicKey::try_decode_protobuf(hash.digest()).ok()?; + public.try_into_ed25519().ok().map(|public| public.to_bytes()) +} + +/// Convert a mixnet core [`PeerId`](CorePeerId) into a libp2p [`PeerId`]. +/// +/// This will succeed only if `peer_id` represents a point on the Ed25519 curve. Returns `None` on +/// failure. +pub fn from_core_peer_id(core_peer_id: &CorePeerId) -> Option<PeerId> { + let public = libp2p_identity::ed25519::PublicKey::try_from_bytes(core_peer_id).ok()?; + let public: libp2p_identity::PublicKey = public.into(); + Some(public.into()) +} diff --git a/substrate/client/mixnet/src/protocol.rs b/substrate/client/mixnet/src/protocol.rs new file mode 100644 index 0000000000000..555c267b86e0c --- /dev/null +++ b/substrate/client/mixnet/src/protocol.rs @@ -0,0 +1,42 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use super::config::Config; +use mixnet::core::PACKET_SIZE; +use sc_network::{config::NonDefaultSetConfig, ProtocolName}; + +/// Returns the protocol name to use for the mixnet controlled by the given chain. +pub fn protocol_name(genesis_hash: &[u8], fork_id: Option<&str>) -> ProtocolName { + let name = if let Some(fork_id) = fork_id { + format!("/{}/{}/mixnet/1", array_bytes::bytes2hex("", genesis_hash), fork_id) + } else { + format!("/{}/mixnet/1", array_bytes::bytes2hex("", genesis_hash)) + }; + name.into() +} + +/// Returns the peers set configuration for the mixnet protocol. +pub fn peers_set_config(name: ProtocolName, config: &Config) -> NonDefaultSetConfig { + let mut set_config = NonDefaultSetConfig::new(name, PACKET_SIZE as u64); + if config.substrate.num_gateway_slots != 0 { + // out_peers is always 0; we are only interested in connecting to mixnodes, which we do by + // setting them as reserved nodes + set_config.allow_non_reserved(config.substrate.num_gateway_slots, 0); + } + set_config +} diff --git a/substrate/client/mixnet/src/request.rs b/substrate/client/mixnet/src/request.rs new file mode 100644 index 0000000000000..18a74c7ea5cf1 --- /dev/null +++ b/substrate/client/mixnet/src/request.rs @@ -0,0 +1,119 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Sender-side request logic. Some things from this module are also used on the receiver side, eg +//! [`extrinsic_delay`], but most of the receiver-side request logic lives elsewhere. + +use super::{config::SubstrateConfig, error::Error}; +use blake2::{ + digest::{consts::U16, Mac}, + Blake2bMac, +}; +use codec::{Decode, DecodeAll}; +use futures::channel::oneshot; +use log::debug; +use mixnet::core::{Delay, MessageId, PostErr, Scattered}; +use sp_core::Bytes; +use std::time::Duration; + +const LOG_TARGET: &str = "mixnet"; + +fn send_err<T>(reply_sender: oneshot::Sender<Result<T, Error>>, err: Error) { + if let Err(Err(err)) = reply_sender.send(Err(err)) { + debug!(target: LOG_TARGET, "Failed to inform requester of error: {err}"); + } +} + +fn send_reply<T: Decode>(reply_sender: oneshot::Sender<Result<T, Error>>, mut data: &[u8]) { + let res = match Result::decode_all(&mut data) { + Ok(res) => res.map_err(Error::Remote), + Err(_) => Err(Error::BadReply), + }; + match reply_sender.send(res) { + Ok(_) => (), + Err(Ok(_)) => debug!(target: LOG_TARGET, "Failed to send reply to requester"), + Err(Err(err)) => debug!(target: LOG_TARGET, "Failed to inform requester of error: {err}"), + } +} + +/// First byte of a submit extrinsic request, identifying it as such. +pub const SUBMIT_EXTRINSIC: u8 = 1; + +const EXTRINSIC_DELAY_PERSONA: &[u8; 16] = b"submit-extrn-dly"; + +/// Returns the artificial delay that should be inserted between receipt of a submit extrinsic +/// request with the given message ID and import of the extrinsic into the local transaction pool. +pub fn extrinsic_delay(message_id: &MessageId, config: &SubstrateConfig) -> Duration { + let h = Blake2bMac::<U16>::new_with_salt_and_personal(message_id, b"", EXTRINSIC_DELAY_PERSONA) + .expect("Key, salt, and persona sizes are fixed and small enough"); + let delay = Delay::exp(h.finalize().into_bytes().as_ref()); + delay.to_duration(config.mean_extrinsic_delay) +} + +/// Request parameters and local reply channel. Stored by the +/// [`RequestManager`](mixnet::request_manager::RequestManager). +pub enum Request { + SubmitExtrinsic { extrinsic: Bytes, reply_sender: oneshot::Sender<Result<(), Error>> }, +} + +impl Request { + /// Forward an error to the user of the mixnet service. + fn send_err(self, err: Error) { + match self { + Request::SubmitExtrinsic { reply_sender, .. } => send_err(reply_sender, err), + } + } + + /// Forward a reply to the user of the mixnet service. + pub fn send_reply(self, data: &[u8]) { + match self { + Request::SubmitExtrinsic { reply_sender, .. } => send_reply(reply_sender, data), + } + } +} + +impl mixnet::request_manager::Request for Request { + type Context = SubstrateConfig; + + fn with_data<T>(&self, f: impl FnOnce(Scattered<u8>) -> T, _context: &Self::Context) -> T { + match self { + Request::SubmitExtrinsic { extrinsic, .. } => + f([&[SUBMIT_EXTRINSIC], extrinsic.as_ref()].as_slice().into()), + } + } + + fn num_surbs(&self, context: &Self::Context) -> usize { + match self { + Request::SubmitExtrinsic { .. } => context.surb_factor, + } + } + + fn handling_delay(&self, message_id: &MessageId, context: &Self::Context) -> Duration { + match self { + Request::SubmitExtrinsic { .. } => extrinsic_delay(message_id, context), + } + } + + fn handle_post_err(self, err: PostErr, _context: &Self::Context) { + self.send_err(err.into()); + } + + fn handle_retry_limit_reached(self, _context: &Self::Context) { + self.send_err(Error::NoReply); + } +} diff --git a/substrate/client/mixnet/src/run.rs b/substrate/client/mixnet/src/run.rs new file mode 100644 index 0000000000000..09020469d5eee --- /dev/null +++ b/substrate/client/mixnet/src/run.rs @@ -0,0 +1,388 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Top-level mixnet service function. + +use super::{ + api::ApiBackend, + config::{Config, SubstrateConfig}, + error::RemoteErr, + extrinsic_queue::ExtrinsicQueue, + maybe_inf_delay::MaybeInfDelay, + packet_dispatcher::PacketDispatcher, + peer_id::to_core_peer_id, + request::{extrinsic_delay, Request, SUBMIT_EXTRINSIC}, + sync_with_runtime::sync_with_runtime, +}; +use codec::{Decode, DecodeAll, Encode}; +use futures::{ + future::{pending, Either}, + stream::FuturesUnordered, + StreamExt, +}; +use log::{debug, error, trace, warn}; +use mixnet::{ + core::{Events, Message, Mixnet, Packet}, + reply_manager::{ReplyContext, ReplyManager}, + request_manager::RequestManager, +}; +use sc_client_api::{BlockchainEvents, HeaderBackend}; +use sc_network::{ + Event::{NotificationStreamClosed, NotificationStreamOpened, NotificationsReceived}, + NetworkEventStream, NetworkNotification, NetworkPeers, NetworkStateInfo, ProtocolName, +}; +use sc_transaction_pool_api::{ + LocalTransactionPool, OffchainTransactionPoolFactory, TransactionPool, +}; +use sp_api::{ApiExt, ProvideRuntimeApi}; +use sp_consensus::SyncOracle; +use sp_keystore::{KeystoreExt, KeystorePtr}; +use sp_mixnet::{runtime_api::MixnetApi, types::Mixnode}; +use sp_runtime::{ + traits::{Block, Header}, + transaction_validity::TransactionSource, + Saturating, +}; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; + +const LOG_TARGET: &str = "mixnet"; + +const MIN_BLOCKS_BETWEEN_REGISTRATION_ATTEMPTS: u32 = 3; + +fn complete_submit_extrinsic<X>( + reply_manager: &mut ReplyManager, + reply_context: ReplyContext, + data: Result<(), RemoteErr>, + mixnet: &mut Mixnet<X>, +) { + reply_manager.complete(reply_context, data.encode(), mixnet); +} + +fn handle_packet<X, E: Decode>( + packet: &Packet, + mixnet: &mut Mixnet<X>, + request_manager: &mut RequestManager<Request>, + reply_manager: &mut ReplyManager, + extrinsic_queue: &mut ExtrinsicQueue<E>, + config: &SubstrateConfig, +) { + match mixnet.handle_packet(packet) { + Some(Message::Request(message)) => { + let Some((reply_context, data)) = reply_manager.insert(message, mixnet) else { return }; + + match data.as_slice() { + [SUBMIT_EXTRINSIC, encoded_extrinsic @ ..] => { + if !extrinsic_queue.has_space() { + debug!(target: LOG_TARGET, "No space in extrinsic queue; dropping request"); + // We don't send a reply in this case; we want the requester to retry + reply_manager.abandon(reply_context); + return + } + + // Decode the extrinsic + let mut encoded_extrinsic = encoded_extrinsic; + let extrinsic = match E::decode_all(&mut encoded_extrinsic) { + Ok(extrinsic) => extrinsic, + Err(err) => { + complete_submit_extrinsic( + reply_manager, + reply_context, + Err(RemoteErr::Decode(format!("Bad extrinsic: {}", err))), + mixnet, + ); + return + }, + }; + + let deadline = + Instant::now() + extrinsic_delay(reply_context.message_id(), config); + extrinsic_queue.insert(deadline, extrinsic, reply_context); + }, + _ => { + debug!(target: LOG_TARGET, "Unrecognised request; discarding"); + // To keep things simple we don't bother sending a reply in this case. The + // requester will give up and try another mixnode eventually. + reply_manager.abandon(reply_context); + }, + } + }, + Some(Message::Reply(message)) => { + let Some(request) = request_manager.remove(&message.request_id) else { + trace!( + target: LOG_TARGET, + "Received reply to already-completed request with message ID {:x?}", + message.request_id + ); + return + }; + request.send_reply(&message.data); + }, + None => (), + } +} + +fn time_until(instant: Instant) -> Duration { + instant.saturating_duration_since(Instant::now()) +} + +/// Run the mixnet service. If `keystore` is `None`, the service will not attempt to register the +/// local node as a mixnode, even if `config.register` is `true`. +pub async fn run<B, C, S, N, P>( + config: Config, + mut api_backend: ApiBackend, + client: Arc<C>, + sync: Arc<S>, + network: Arc<N>, + protocol_name: ProtocolName, + transaction_pool: Arc<P>, + keystore: Option<KeystorePtr>, +) where + B: Block, + C: BlockchainEvents<B> + ProvideRuntimeApi<B> + HeaderBackend<B>, + C::Api: MixnetApi<B>, + S: SyncOracle, + N: NetworkStateInfo + NetworkEventStream + NetworkNotification + NetworkPeers, + P: TransactionPool<Block = B> + LocalTransactionPool<Block = B> + 'static, +{ + let local_peer_id = network.local_peer_id(); + let Some(local_peer_id) = to_core_peer_id(&local_peer_id) else { + error!(target: LOG_TARGET, + "Failed to convert libp2p local peer ID {local_peer_id} to mixnet peer ID; \ + mixnet not running"); + return + }; + + let offchain_transaction_pool_factory = + OffchainTransactionPoolFactory::new(transaction_pool.clone()); + + let mut mixnet = Mixnet::new(config.core); + // It would make sense to reset this to 0 when the session changes, but registrations aren't + // allowed at the start of a session anyway, so it doesn't really matter + let mut min_register_block = 0u32.into(); + let mut packet_dispatcher = PacketDispatcher::new(&local_peer_id); + let mut request_manager = RequestManager::new(config.request_manager); + let mut reply_manager = ReplyManager::new(config.reply_manager); + let mut extrinsic_queue = ExtrinsicQueue::new(config.substrate.extrinsic_queue_capacity); + + let mut finality_notifications = client.finality_notification_stream(); + // Import notifications only used for triggering registration attempts + let mut import_notifications = if config.substrate.register && keystore.is_some() { + Some(client.import_notification_stream()) + } else { + None + }; + let mut network_events = network.event_stream("mixnet").fuse(); + let mut next_forward_packet_delay = MaybeInfDelay::new(None); + let mut next_authored_packet_delay = MaybeInfDelay::new(None); + let mut ready_peers = FuturesUnordered::new(); + let mut next_retry_delay = MaybeInfDelay::new(None); + let mut next_extrinsic_delay = MaybeInfDelay::new(None); + let mut submit_extrinsic_results = FuturesUnordered::new(); + + loop { + let mut next_request = if request_manager.has_space() { + Either::Left(api_backend.request_receiver.select_next_some()) + } else { + Either::Right(pending()) + }; + + let mut next_import_notification = import_notifications.as_mut().map_or_else( + || Either::Right(pending()), + |notifications| Either::Left(notifications.select_next_some()), + ); + + futures::select! { + request = next_request => + request_manager.insert(request, &mut mixnet, &packet_dispatcher, &config.substrate), + + notification = finality_notifications.select_next_some() => { + // To avoid trying to connect to old mixnodes, ignore finality notifications while + // offline or major syncing. This is a bit racy but should be good enough. + if !sync.is_offline() && !sync.is_major_syncing() { + let api = client.runtime_api(); + sync_with_runtime(&mut mixnet, api, notification.hash); + request_manager.update_session_status( + &mut mixnet, &packet_dispatcher, &config.substrate); + } + } + + notification = next_import_notification => { + if notification.is_new_best && (*notification.header.number() >= min_register_block) { + let mut api = client.runtime_api(); + api.register_extension(KeystoreExt(keystore.clone().expect( + "Import notification stream only setup if we have a keystore"))); + api.register_extension(offchain_transaction_pool_factory + .offchain_transaction_pool(notification.hash)); + let session_index = mixnet.session_status().current_index; + let mixnode = Mixnode { + kx_public: *mixnet.next_kx_public(), + peer_id: local_peer_id, + external_addresses: network.external_addresses().into_iter() + .map(|addr| addr.to_string().into_bytes()).collect(), + }; + match api.maybe_register(notification.hash, session_index, mixnode) { + Ok(true) => min_register_block = notification.header.number().saturating_add( + MIN_BLOCKS_BETWEEN_REGISTRATION_ATTEMPTS.into()), + Ok(false) => (), + Err(err) => debug!(target: LOG_TARGET, + "Error trying to register for the next session: {err}"), + } + } + } + + event = network_events.select_next_some() => match event { + NotificationStreamOpened { remote, protocol, .. } + if protocol == protocol_name => packet_dispatcher.add_peer(&remote), + NotificationStreamClosed { remote, protocol } + if protocol == protocol_name => packet_dispatcher.remove_peer(&remote), + NotificationsReceived { remote, messages } => { + for message in messages { + if message.0 == protocol_name { + match message.1.as_ref().try_into() { + Ok(packet) => handle_packet(packet, + &mut mixnet, &mut request_manager, &mut reply_manager, + &mut extrinsic_queue, &config.substrate), + Err(_) => debug!(target: LOG_TARGET, + "Dropped incorrectly sized packet ({} bytes) from {remote}", + message.1.len(), + ), + } + } + } + } + _ => () + }, + + _ = next_forward_packet_delay => { + if let Some(packet) = mixnet.pop_next_forward_packet() { + if let Some(ready_peer) = packet_dispatcher.dispatch(packet) { + if let Some(fut) = ready_peer.send_packet(&*network, protocol_name.clone()) { + ready_peers.push(fut); + } + } + } else { + warn!(target: LOG_TARGET, + "Next forward packet deadline reached, but no packet in queue; \ + this is a bug"); + } + } + + _ = next_authored_packet_delay => { + if let Some(packet) = mixnet.pop_next_authored_packet(&packet_dispatcher) { + if let Some(ready_peer) = packet_dispatcher.dispatch(packet) { + if let Some(fut) = ready_peer.send_packet(&*network, protocol_name.clone()) { + ready_peers.push(fut); + } + } + } + } + + ready_peer = ready_peers.select_next_some() => { + if let Some(ready_peer) = ready_peer { + if let Some(fut) = ready_peer.send_packet(&*network, protocol_name.clone()) { + ready_peers.push(fut); + } + } + } + + _ = next_retry_delay => { + if !request_manager.pop_next_retry(&mut mixnet, &packet_dispatcher, &config.substrate) { + warn!(target: LOG_TARGET, + "Next retry deadline reached, but no request in retry queue; \ + this is a bug"); + } + } + + _ = next_extrinsic_delay => { + if let Some((extrinsic, reply_context)) = extrinsic_queue.pop() { + if submit_extrinsic_results.len() < config.substrate.max_pending_extrinsics { + let fut = transaction_pool.submit_one( + client.info().best_hash, + TransactionSource::External, + extrinsic); + submit_extrinsic_results.push(async move { + (fut.await, reply_context) + }); + } else { + // There are already too many pending extrinsics, just drop this one. We + // don't send a reply; we want the requester to retry. + debug!(target: LOG_TARGET, + "Too many pending extrinsics; dropped submit extrinsic request"); + reply_manager.abandon(reply_context); + } + } else { + warn!(target: LOG_TARGET, + "Next extrinsic deadline reached, but no extrinsic in queue; \ + this is a bug"); + } + } + + res_reply_context = submit_extrinsic_results.select_next_some() => { + let (res, reply_context) = res_reply_context; + let res = match res { + Ok(_) => Ok(()), + Err(err) => Err(RemoteErr::Other(err.to_string())), + }; + complete_submit_extrinsic(&mut reply_manager, reply_context, res, &mut mixnet); + } + } + + let events = mixnet.take_events(); + if !events.is_empty() { + if events.contains(Events::RESERVED_PEERS_CHANGED) { + let reserved_peer_addrs = mixnet + .reserved_peers() + .flat_map(|mixnode| mixnode.extra.iter()) // External addresses + .cloned() + .collect(); + if let Err(err) = + network.set_reserved_peers(protocol_name.clone(), reserved_peer_addrs) + { + debug!(target: LOG_TARGET, "Setting reserved peers failed: {err}"); + } + } + if events.contains(Events::NEXT_FORWARD_PACKET_DEADLINE_CHANGED) { + next_forward_packet_delay + .reset(mixnet.next_forward_packet_deadline().map(time_until)); + } + if events.contains(Events::NEXT_AUTHORED_PACKET_DEADLINE_CHANGED) { + next_authored_packet_delay.reset(mixnet.next_authored_packet_delay()); + } + if events.contains(Events::SPACE_IN_AUTHORED_PACKET_QUEUE) { + // Note this may cause the next retry deadline to change, but should not trigger + // any mixnet events + request_manager.process_post_queues( + &mut mixnet, + &packet_dispatcher, + &config.substrate, + ); + } + } + + if request_manager.next_retry_deadline_changed() { + next_retry_delay.reset(request_manager.next_retry_deadline().map(time_until)); + } + + if extrinsic_queue.next_deadline_changed() { + next_extrinsic_delay.reset(extrinsic_queue.next_deadline().map(time_until)); + } + } +} diff --git a/substrate/client/mixnet/src/sync_with_runtime.rs b/substrate/client/mixnet/src/sync_with_runtime.rs new file mode 100644 index 0000000000000..4a80b3c75f43d --- /dev/null +++ b/substrate/client/mixnet/src/sync_with_runtime.rs @@ -0,0 +1,228 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! [`sync_with_runtime`] synchronises the session status and mixnode sets from the blockchain +//! runtime to the core mixnet state. It is called every time a block is finalised. + +use super::peer_id::from_core_peer_id; +use libp2p_identity::PeerId; +use log::{debug, info}; +use mixnet::core::{ + Mixnet, Mixnode as CoreMixnode, MixnodesErr as CoreMixnodesErr, RelSessionIndex, + SessionPhase as CoreSessionPhase, SessionStatus as CoreSessionStatus, +}; +use multiaddr::{multiaddr, Multiaddr, Protocol}; +use sp_api::{ApiError, ApiRef}; +use sp_mixnet::{ + runtime_api::MixnetApi, + types::{ + Mixnode as RuntimeMixnode, MixnodesErr as RuntimeMixnodesErr, + SessionPhase as RuntimeSessionPhase, SessionStatus as RuntimeSessionStatus, + }, +}; +use sp_runtime::traits::Block; + +const LOG_TARGET: &str = "mixnet"; + +/// Convert a [`RuntimeSessionStatus`] to a [`CoreSessionStatus`]. +/// +/// The [`RuntimeSessionStatus`] and [`CoreSessionStatus`] types are effectively the same. +/// [`RuntimeSessionStatus`] is used in the runtime to avoid depending on the [`mixnet`] crate +/// there. +fn to_core_session_status(status: RuntimeSessionStatus) -> CoreSessionStatus { + CoreSessionStatus { + current_index: status.current_index, + phase: match status.phase { + RuntimeSessionPhase::CoverToCurrent => CoreSessionPhase::CoverToCurrent, + RuntimeSessionPhase::RequestsToCurrent => CoreSessionPhase::RequestsToCurrent, + RuntimeSessionPhase::CoverToPrev => CoreSessionPhase::CoverToPrev, + RuntimeSessionPhase::DisconnectFromPrev => CoreSessionPhase::DisconnectFromPrev, + }, + } +} + +fn parse_external_addresses(external_addresses: Vec<Vec<u8>>) -> Vec<Multiaddr> { + external_addresses + .into_iter() + .flat_map(|addr| { + let addr = match String::from_utf8(addr) { + Ok(addr) => addr, + Err(addr) => { + debug!( + target: LOG_TARGET, + "Mixnode external address {:x?} is not valid UTF-8", + addr.into_bytes(), + ); + return None + }, + }; + match addr.parse() { + Ok(addr) => Some(addr), + Err(err) => { + debug!( + target: LOG_TARGET, + "Could not parse mixnode address {addr}: {err}", + ); + None + }, + } + }) + .collect() +} + +/// Modify `external_addresses` such that there is at least one address and the final component of +/// each address matches `peer_id`. +fn fixup_external_addresses(external_addresses: &mut Vec<Multiaddr>, peer_id: &PeerId) { + // Ensure the final component of each address matches peer_id + external_addresses.retain_mut(|addr| match PeerId::try_from_multiaddr(addr) { + Some(addr_peer_id) if addr_peer_id == *peer_id => true, + Some(_) => { + debug!( + target: LOG_TARGET, + "Mixnode address {} does not match mixnode peer ID {}, ignoring", + addr, + peer_id + ); + false + }, + None if matches!(addr.iter().last(), Some(Protocol::P2p(_))) => { + debug!( + target: LOG_TARGET, + "Mixnode address {} has unrecognised P2P protocol, ignoring", + addr + ); + false + }, + None => { + addr.push(Protocol::P2p(*peer_id.as_ref())); + true + }, + }); + + // If there are no addresses, insert one consisting of just the peer ID + if external_addresses.is_empty() { + external_addresses.push(multiaddr!(P2p(*peer_id.as_ref()))); + } +} + +/// Convert a [`RuntimeMixnode`] to a [`CoreMixnode`]. If the conversion fails, an error message is +/// logged, but a [`CoreMixnode`] is still returned. +/// +/// It would be possible to handle conversion failure in a better way, but this would complicate +/// things for what should be a rare case. Note that even if the conversion here succeeds, there is +/// no guarantee that we will be able to connect to the mixnode or send packets to it. The most +/// common failure case is expected to be that a mixnode is simply unreachable over the network. +fn into_core_mixnode(mixnode: RuntimeMixnode) -> CoreMixnode<Vec<Multiaddr>> { + let external_addresses = if let Some(peer_id) = from_core_peer_id(&mixnode.peer_id) { + let mut external_addresses = parse_external_addresses(mixnode.external_addresses); + fixup_external_addresses(&mut external_addresses, &peer_id); + external_addresses + } else { + debug!( + target: LOG_TARGET, + "Failed to convert mixnet peer ID {:x?} to libp2p peer ID", + mixnode.peer_id, + ); + Vec::new() + }; + + CoreMixnode { + kx_public: mixnode.kx_public, + peer_id: mixnode.peer_id, + extra: external_addresses, + } +} + +fn maybe_set_mixnodes( + mixnet: &mut Mixnet<Vec<Multiaddr>>, + rel_session_index: RelSessionIndex, + mixnodes: &dyn Fn() -> Result<Result<Vec<RuntimeMixnode>, RuntimeMixnodesErr>, ApiError>, +) { + let current_session_index = mixnet.session_status().current_index; + mixnet.maybe_set_mixnodes(rel_session_index, &mut || { + // Note that RelSessionIndex::Prev + 0 would panic, but this closure will not get called in + // that case so we are fine. Do not move this out of the closure! + let session_index = rel_session_index + current_session_index; + match mixnodes() { + Ok(Ok(mixnodes)) => Ok(mixnodes.into_iter().map(into_core_mixnode).collect()), + Ok(Err(err)) => { + info!(target: LOG_TARGET, "Session {session_index}: Mixnet disabled: {err}"); + Err(CoreMixnodesErr::Permanent) // Disable the session slot + }, + Err(err) => { + debug!( + target: LOG_TARGET, + "Session {session_index}: Error getting mixnodes from runtime: {err}" + ); + Err(CoreMixnodesErr::Transient) // Just leave the session slot empty; try again next block + }, + } + }); +} + +pub fn sync_with_runtime<B, A>(mixnet: &mut Mixnet<Vec<Multiaddr>>, api: ApiRef<A>, hash: B::Hash) +where + B: Block, + A: MixnetApi<B>, +{ + let session_status = match api.session_status(hash) { + Ok(session_status) => session_status, + Err(err) => { + debug!(target: LOG_TARGET, "Error getting session status from runtime: {err}"); + return + }, + }; + mixnet.set_session_status(to_core_session_status(session_status)); + + maybe_set_mixnodes(mixnet, RelSessionIndex::Prev, &|| api.prev_mixnodes(hash)); + maybe_set_mixnodes(mixnet, RelSessionIndex::Current, &|| api.current_mixnodes(hash)); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fixup_empty_external_addresses() { + let peer_id = PeerId::random(); + let mut external_addresses = Vec::new(); + fixup_external_addresses(&mut external_addresses, &peer_id); + assert_eq!(external_addresses, vec![multiaddr!(P2p(peer_id))]); + } + + #[test] + fn fixup_misc_external_addresses() { + let peer_id = PeerId::random(); + let other_peer_id = PeerId::random(); + let mut external_addresses = vec![ + multiaddr!(Tcp(0u16), P2p(peer_id)), + multiaddr!(Tcp(1u16), P2p(other_peer_id)), + multiaddr!(Tcp(2u16)), + Multiaddr::empty(), + ]; + fixup_external_addresses(&mut external_addresses, &peer_id); + assert_eq!( + external_addresses, + vec![ + multiaddr!(Tcp(0u16), P2p(peer_id)), + multiaddr!(Tcp(2u16), P2p(peer_id)), + multiaddr!(P2p(peer_id)), + ] + ); + } +} diff --git a/substrate/client/network/src/protocol_controller.rs b/substrate/client/network/src/protocol_controller.rs index c9baa0a77d4ba..3a305011ded02 100644 --- a/substrate/client/network/src/protocol_controller.rs +++ b/substrate/client/network/src/protocol_controller.rs @@ -493,8 +493,8 @@ impl ProtocolController { } } - /// Remove the peer from the set of reserved peers. The peer is moved to the set of regular - /// nodes. + /// Remove the peer from the set of reserved peers. The peer is either moved to the set of + /// regular nodes or disconnected. fn on_remove_reserved_peer(&mut self, peer_id: PeerId) { let state = match self.reserved_nodes.remove(&peer_id) { Some(state) => state, @@ -508,7 +508,14 @@ impl ProtocolController { }; if let PeerState::Connected(direction) = state { - if self.reserved_only { + // Disconnect if we're at (or over) the regular node limit + let disconnect = self.reserved_only || + match direction { + Direction::Inbound => self.num_in >= self.max_in, + Direction::Outbound => self.num_out >= self.max_out, + }; + + if disconnect { // Disconnect the node. trace!( target: LOG_TARGET, diff --git a/substrate/client/rpc-api/Cargo.toml b/substrate/client/rpc-api/Cargo.toml index a2ee090b1c239..9dca2e72fcdd5 100644 --- a/substrate/client/rpc-api/Cargo.toml +++ b/substrate/client/rpc-api/Cargo.toml @@ -19,6 +19,7 @@ serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" thiserror = "1.0" sc-chain-spec = { path = "../chain-spec" } +sc-mixnet = { path = "../mixnet" } sc-transaction-pool-api = { path = "../transaction-pool/api" } sp-core = { path = "../../primitives/core" } sp-rpc = { path = "../../primitives/rpc" } diff --git a/substrate/client/rpc-api/src/error.rs b/substrate/client/rpc-api/src/error.rs index 72941e3145b94..58b75b8fb1691 100644 --- a/substrate/client/rpc-api/src/error.rs +++ b/substrate/client/rpc-api/src/error.rs @@ -25,4 +25,5 @@ pub mod base { pub const OFFCHAIN: i32 = 5000; pub const DEV: i32 = 6000; pub const STATEMENT: i32 = 7000; + pub const MIXNET: i32 = 8000; } diff --git a/substrate/client/rpc-api/src/lib.rs b/substrate/client/rpc-api/src/lib.rs index b99c237dc859b..32120d37902dd 100644 --- a/substrate/client/rpc-api/src/lib.rs +++ b/substrate/client/rpc-api/src/lib.rs @@ -31,6 +31,7 @@ pub mod author; pub mod chain; pub mod child_state; pub mod dev; +pub mod mixnet; pub mod offchain; pub mod state; pub mod statement; diff --git a/substrate/client/rpc-api/src/mixnet/error.rs b/substrate/client/rpc-api/src/mixnet/error.rs new file mode 100644 index 0000000000000..0dde5f32e6139 --- /dev/null +++ b/substrate/client/rpc-api/src/mixnet/error.rs @@ -0,0 +1,48 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Mixnet RPC module errors. + +use jsonrpsee::types::error::{CallError, ErrorObject}; +use sc_mixnet::{PostErr, RemoteErr, TopologyErr}; + +/// Mixnet RPC error type. +pub struct Error(pub sc_mixnet::Error); + +/// Base code for all mixnet errors. +const BASE_ERROR: i32 = crate::error::base::MIXNET; + +impl From<Error> for jsonrpsee::core::Error { + fn from(err: Error) -> Self { + let code = match err.0 { + sc_mixnet::Error::ServiceUnavailable => BASE_ERROR + 1, + sc_mixnet::Error::NoReply => BASE_ERROR + 2, + sc_mixnet::Error::BadReply => BASE_ERROR + 3, + sc_mixnet::Error::Post(PostErr::TooManyFragments) => BASE_ERROR + 101, + sc_mixnet::Error::Post(PostErr::SessionMixnodesNotKnown(_)) => BASE_ERROR + 102, + sc_mixnet::Error::Post(PostErr::SessionDisabled(_)) => BASE_ERROR + 103, + sc_mixnet::Error::Post(PostErr::Topology(TopologyErr::NoConnectedGatewayMixnodes)) => + BASE_ERROR + 151, + sc_mixnet::Error::Post(PostErr::Topology(_)) => BASE_ERROR + 150, + sc_mixnet::Error::Post(_) => BASE_ERROR + 100, + sc_mixnet::Error::Remote(RemoteErr::Other(_)) => BASE_ERROR + 200, + sc_mixnet::Error::Remote(RemoteErr::Decode(_)) => BASE_ERROR + 201, + }; + CallError::Custom(ErrorObject::owned(code, err.0.to_string(), None::<()>)).into() + } +} diff --git a/substrate/client/rpc-api/src/mixnet/mod.rs b/substrate/client/rpc-api/src/mixnet/mod.rs new file mode 100644 index 0000000000000..bc478cf3bf334 --- /dev/null +++ b/substrate/client/rpc-api/src/mixnet/mod.rs @@ -0,0 +1,31 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Substrate mixnet API. + +pub mod error; + +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; +use sp_core::Bytes; + +#[rpc(client, server)] +pub trait MixnetApi { + /// Submit encoded extrinsic over the mixnet for inclusion in block. + #[method(name = "mixnet_submitExtrinsic")] + async fn submit_extrinsic(&self, extrinsic: Bytes) -> RpcResult<()>; +} diff --git a/substrate/client/rpc/Cargo.toml b/substrate/client/rpc/Cargo.toml index 64aaa7c94aacb..dd1120e5b0f86 100644 --- a/substrate/client/rpc/Cargo.toml +++ b/substrate/client/rpc/Cargo.toml @@ -22,6 +22,7 @@ serde_json = "1.0.107" sc-block-builder = { path = "../block-builder" } sc-chain-spec = { path = "../chain-spec" } sc-client-api = { path = "../api" } +sc-mixnet = { path = "../mixnet" } sc-rpc-api = { path = "../rpc-api" } sc-tracing = { path = "../tracing" } sc-transaction-pool-api = { path = "../transaction-pool/api" } diff --git a/substrate/client/rpc/src/lib.rs b/substrate/client/rpc/src/lib.rs index 475fc77a9b5bd..94fdb2d734ff0 100644 --- a/substrate/client/rpc/src/lib.rs +++ b/substrate/client/rpc/src/lib.rs @@ -34,6 +34,7 @@ pub use sc_rpc_api::DenyUnsafe; pub mod author; pub mod chain; pub mod dev; +pub mod mixnet; pub mod offchain; pub mod state; pub mod statement; diff --git a/substrate/client/rpc/src/mixnet/mod.rs b/substrate/client/rpc/src/mixnet/mod.rs new file mode 100644 index 0000000000000..3f3d9c5aa4526 --- /dev/null +++ b/substrate/client/rpc/src/mixnet/mod.rs @@ -0,0 +1,47 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Substrate mixnet API. + +use jsonrpsee::core::{async_trait, RpcResult}; +use sc_mixnet::Api; +use sc_rpc_api::mixnet::error::Error; +pub use sc_rpc_api::mixnet::MixnetApiServer; +use sp_core::Bytes; + +/// Mixnet API. +pub struct Mixnet(futures::lock::Mutex<Api>); + +impl Mixnet { + /// Create a new mixnet API instance. + pub fn new(api: Api) -> Self { + Self(futures::lock::Mutex::new(api)) + } +} + +#[async_trait] +impl MixnetApiServer for Mixnet { + async fn submit_extrinsic(&self, extrinsic: Bytes) -> RpcResult<()> { + // We only hold the lock while pushing the request into the requests channel + let fut = { + let mut api = self.0.lock().await; + api.submit_extrinsic(extrinsic).await + }; + Ok(fut.await.map_err(Error)?) + } +} diff --git a/substrate/frame/mixnet/Cargo.toml b/substrate/frame/mixnet/Cargo.toml new file mode 100644 index 0000000000000..68ffdad20fcbb --- /dev/null +++ b/substrate/frame/mixnet/Cargo.toml @@ -0,0 +1,57 @@ +[package] +description = "FRAME's mixnet pallet" +name = "pallet-mixnet" +version = "0.1.0-dev" +license = "Apache-2.0" +authors = ["Parity Technologies <admin@parity.io>"] +edition = "2021" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive", "max-encoded-len"] } +frame-benchmarking = { default-features = false, optional = true, path = "../benchmarking" } +frame-support = { default-features = false, path = "../support" } +frame-system = { default-features = false, path = "../system" } +log = { version = "0.4.17", default-features = false } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.188", default-features = false, features = ["derive"] } +sp-application-crypto = { default-features = false, path = "../../primitives/application-crypto" } +sp-arithmetic = { default-features = false, path = "../../primitives/arithmetic" } +sp-io = { default-features = false, path = "../../primitives/io" } +sp-mixnet = { default-features = false, path = "../../primitives/mixnet" } +sp-runtime = { default-features = false, path = "../../primitives/runtime" } +sp-std = { default-features = false, path = "../../primitives/std" } + +[features] +default = [ "std" ] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "serde/std", + "sp-application-crypto/std", + "sp-arithmetic/std", + "sp-io/std", + "sp-mixnet/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/substrate/frame/mixnet/README.md b/substrate/frame/mixnet/README.md new file mode 100644 index 0000000000000..59b81851ed11f --- /dev/null +++ b/substrate/frame/mixnet/README.md @@ -0,0 +1,4 @@ +This pallet is responsible for determining the current mixnet session and phase, and the mixnode +set for each session. + +License: Apache-2.0 diff --git a/substrate/frame/mixnet/src/lib.rs b/substrate/frame/mixnet/src/lib.rs new file mode 100644 index 0000000000000..c7a5b624157b8 --- /dev/null +++ b/substrate/frame/mixnet/src/lib.rs @@ -0,0 +1,598 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// 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. + +//! This pallet is responsible for determining the current mixnet session and phase, and the +//! mixnode set for each session. + +#![warn(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + traits::{EstimateNextSessionRotation, Get, OneSessionHandler}, + BoundedVec, +}; +use frame_system::{ + offchain::{SendTransactionTypes, SubmitTransaction}, + pallet_prelude::BlockNumberFor, +}; +pub use pallet::*; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_application_crypto::RuntimeAppPublic; +use sp_arithmetic::traits::{CheckedSub, Saturating, UniqueSaturatedInto, Zero}; +use sp_io::MultiRemovalResults; +use sp_mixnet::types::{ + AuthorityId, AuthoritySignature, KxPublic, Mixnode, MixnodesErr, PeerId, SessionIndex, + SessionPhase, SessionStatus, KX_PUBLIC_SIZE, +}; +use sp_runtime::RuntimeDebug; +use sp_std::{cmp::Ordering, vec::Vec}; + +const LOG_TARGET: &str = "runtime::mixnet"; + +/// Index of an authority in the authority list for a session. +pub type AuthorityIndex = u32; + +//////////////////////////////////////////////////////////////////////////////// +// Bounded mixnode type +//////////////////////////////////////////////////////////////////////////////// + +/// Like [`Mixnode`], but encoded size is bounded. +#[derive( + Clone, Decode, Encode, MaxEncodedLen, PartialEq, TypeInfo, RuntimeDebug, Serialize, Deserialize, +)] +pub struct BoundedMixnode<ExternalAddresses> { + /// Key-exchange public key for the mixnode. + pub kx_public: KxPublic, + /// libp2p peer ID of the mixnode. + pub peer_id: PeerId, + /// External addresses for the mixnode, in multiaddr format, UTF-8 encoded. + pub external_addresses: ExternalAddresses, +} + +impl<MaxExternalAddressSize, MaxExternalAddresses> Into<Mixnode> + for BoundedMixnode<BoundedVec<BoundedVec<u8, MaxExternalAddressSize>, MaxExternalAddresses>> +{ + fn into(self) -> Mixnode { + Mixnode { + kx_public: self.kx_public, + peer_id: self.peer_id, + external_addresses: self + .external_addresses + .into_iter() + .map(BoundedVec::into_inner) + .collect(), + } + } +} + +impl<MaxExternalAddressSize: Get<u32>, MaxExternalAddresses: Get<u32>> From<Mixnode> + for BoundedMixnode<BoundedVec<BoundedVec<u8, MaxExternalAddressSize>, MaxExternalAddresses>> +{ + fn from(mixnode: Mixnode) -> Self { + Self { + kx_public: mixnode.kx_public, + peer_id: mixnode.peer_id, + external_addresses: mixnode + .external_addresses + .into_iter() + .flat_map(|addr| match addr.try_into() { + Ok(addr) => Some(addr), + Err(addr) => { + log::debug!( + target: LOG_TARGET, + "Mixnode external address {addr:x?} too long; discarding", + ); + None + }, + }) + .take(MaxExternalAddresses::get() as usize) + .collect::<Vec<_>>() + .try_into() + .expect("Excess external addresses discarded with take()"), + } + } +} + +/// [`BoundedMixnode`] type for the given configuration. +pub type BoundedMixnodeFor<T> = BoundedMixnode< + BoundedVec< + BoundedVec<u8, <T as Config>::MaxExternalAddressSize>, + <T as Config>::MaxExternalAddressesPerMixnode, + >, +>; + +//////////////////////////////////////////////////////////////////////////////// +// Registration type +//////////////////////////////////////////////////////////////////////////////// + +/// A mixnode registration. A registration transaction is formed from one of these plus an +/// [`AuthoritySignature`]. +#[derive(Clone, Decode, Encode, PartialEq, TypeInfo, RuntimeDebug)] +pub struct Registration<BlockNumber, BoundedMixnode> { + /// Block number at the time of creation. When a registration transaction fails to make it on + /// to the chain for whatever reason, we send out another one. We want this one to have a + /// different hash in case the earlier transaction got banned somewhere; including the block + /// number is a simple way of achieving this. + pub block_number: BlockNumber, + /// The session during which this registration should be processed. Note that on success the + /// mixnode is registered for the _following_ session. + pub session_index: SessionIndex, + /// The index in the next session's authority list of the authority registering the mixnode. + pub authority_index: AuthorityIndex, + /// Mixnode information to register for the following session. + pub mixnode: BoundedMixnode, +} + +/// [`Registration`] type for the given configuration. +pub type RegistrationFor<T> = Registration<BlockNumberFor<T>, BoundedMixnodeFor<T>>; + +//////////////////////////////////////////////////////////////////////////////// +// Misc helper funcs +//////////////////////////////////////////////////////////////////////////////// + +fn check_removed_all(res: MultiRemovalResults) { + debug_assert!(res.maybe_cursor.is_none()); +} + +fn twox<BlockNumber: UniqueSaturatedInto<u64>>( + block_number: BlockNumber, + kx_public: &KxPublic, +) -> u64 { + let block_number: u64 = block_number.unique_saturated_into(); + let mut data = [0; 8 + KX_PUBLIC_SIZE]; + data[..8].copy_from_slice(&block_number.to_le_bytes()); + data[8..].copy_from_slice(kx_public); + u64::from_le_bytes(sp_io::hashing::twox_64(&data)) +} + +//////////////////////////////////////////////////////////////////////////////// +// The pallet +//////////////////////////////////////////////////////////////////////////////// + +#[frame_support::pallet(dev_mode)] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet<T>(_); + + #[pallet::config] + pub trait Config: frame_system::Config + SendTransactionTypes<Call<Self>> { + /// The maximum number of authorities per session. + #[pallet::constant] + type MaxAuthorities: Get<AuthorityIndex>; + + /// The maximum size of one of a mixnode's external addresses. + #[pallet::constant] + type MaxExternalAddressSize: Get<u32>; + + /// The maximum number of external addresses for a mixnode. + #[pallet::constant] + type MaxExternalAddressesPerMixnode: Get<u32>; + + /// Session progress/length estimation. Used to determine when to send registration + /// transactions and the longevity of these transactions. + type NextSessionRotation: EstimateNextSessionRotation<BlockNumberFor<Self>>; + + /// Length of the first phase of each session (`CoverToCurrent`), in blocks. + #[pallet::constant] + type NumCoverToCurrentBlocks: Get<BlockNumberFor<Self>>; + + /// Length of the second phase of each session (`RequestsToCurrent`), in blocks. + #[pallet::constant] + type NumRequestsToCurrentBlocks: Get<BlockNumberFor<Self>>; + + /// Length of the third phase of each session (`CoverToPrev`), in blocks. + #[pallet::constant] + type NumCoverToPrevBlocks: Get<BlockNumberFor<Self>>; + + /// The number of "slack" blocks at the start of each session, during which + /// [`maybe_register`](Pallet::maybe_register) will not attempt to post registration + /// transactions. + #[pallet::constant] + type NumRegisterStartSlackBlocks: Get<BlockNumberFor<Self>>; + + /// The number of "slack" blocks at the end of each session. + /// [`maybe_register`](Pallet::maybe_register) will try to register before this slack + /// period, but may post registration transactions during the slack period as a last + /// resort. + #[pallet::constant] + type NumRegisterEndSlackBlocks: Get<BlockNumberFor<Self>>; + + /// Priority of unsigned transactions used to register mixnodes. + #[pallet::constant] + type RegistrationPriority: Get<TransactionPriority>; + + /// Minimum number of mixnodes. If there are fewer than this many mixnodes registered for a + /// session, the mixnet will not be active during the session. + #[pallet::constant] + type MinMixnodes: Get<u32>; + } + + /// Index of the current session. This may be offset relative to the session index tracked by + /// eg `pallet_session`; mixnet session indices are independent. + #[pallet::storage] + pub(crate) type CurrentSessionIndex<T> = StorageValue<_, SessionIndex, ValueQuery>; + + /// Block in which the current session started. + #[pallet::storage] + pub(crate) type CurrentSessionStartBlock<T> = StorageValue<_, BlockNumberFor<T>, ValueQuery>; + + /// Authority list for the next session. + #[pallet::storage] + pub(crate) type NextAuthorityIds<T> = StorageMap<_, Identity, AuthorityIndex, AuthorityId>; + + /// Mixnode sets by session index. Only the mixnode sets for the previous, current, and next + /// sessions are kept; older sets are discarded. + /// + /// The mixnodes in each set are keyed by authority index so we can easily check if an + /// authority has registered a mixnode. The authority indices should only be used during + /// registration; the authority indices for the very first session are made up. + #[pallet::storage] + pub(crate) type Mixnodes<T> = + StorageDoubleMap<_, Identity, SessionIndex, Identity, AuthorityIndex, BoundedMixnodeFor<T>>; + + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig<T: Config> { + /// The mixnode set for the very first session. + pub mixnodes: BoundedVec<BoundedMixnodeFor<T>, T::MaxAuthorities>, + } + + #[pallet::genesis_build] + impl<T: Config> BuildGenesisConfig for GenesisConfig<T> { + fn build(&self) { + assert!( + Mixnodes::<T>::iter_prefix_values(0).next().is_none(), + "Initial mixnodes already set" + ); + for (i, mixnode) in self.mixnodes.iter().enumerate() { + // We just make up authority indices here. This doesn't matter as authority indices + // are only used during registration to check an authority doesn't register twice. + Mixnodes::<T>::insert(0, i as AuthorityIndex, mixnode); + } + } + } + + #[pallet::call] + impl<T: Config> Pallet<T> { + /// Register a mixnode for the following session. + #[pallet::call_index(0)] + #[pallet::weight(1)] // TODO + pub fn register( + origin: OriginFor<T>, + registration: RegistrationFor<T>, + _signature: AuthoritySignature, + ) -> DispatchResult { + ensure_none(origin)?; + + // Checked by ValidateUnsigned + debug_assert_eq!(registration.session_index, CurrentSessionIndex::<T>::get()); + debug_assert!(registration.authority_index < T::MaxAuthorities::get()); + + Mixnodes::<T>::insert( + // Registering for the _following_ session + registration.session_index + 1, + registration.authority_index, + registration.mixnode, + ); + + Ok(()) + } + } + + #[pallet::validate_unsigned] + impl<T: Config> ValidateUnsigned for Pallet<T> { + type Call = Call<T>; + + fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { + let Self::Call::register { registration, signature } = call else { + return InvalidTransaction::Call.into() + }; + + // Check session index matches + match registration.session_index.cmp(&CurrentSessionIndex::<T>::get()) { + Ordering::Greater => return InvalidTransaction::Future.into(), + Ordering::Less => return InvalidTransaction::Stale.into(), + Ordering::Equal => (), + } + + // Check authority index is valid + if registration.authority_index >= T::MaxAuthorities::get() { + return InvalidTransaction::BadProof.into() + } + let Some(authority_id) = NextAuthorityIds::<T>::get(registration.authority_index) + else { + return InvalidTransaction::BadProof.into() + }; + + // Check the authority hasn't registered a mixnode yet + if Self::already_registered(registration.session_index, registration.authority_index) { + return InvalidTransaction::Stale.into() + } + + // Check signature. Note that we don't use regular signed transactions for registration + // as we don't want validators to have to pay to register. Spam is prevented by only + // allowing one registration per session per validator (see above). + let signature_ok = registration.using_encoded(|encoded_registration| { + authority_id.verify(&encoded_registration, signature) + }); + if !signature_ok { + return InvalidTransaction::BadProof.into() + } + + ValidTransaction::with_tag_prefix("MixnetRegistration") + .priority(T::RegistrationPriority::get()) + // Include both authority index _and_ ID in tag in case of forks with different + // authority lists + .and_provides(( + registration.session_index, + registration.authority_index, + authority_id, + )) + .longevity( + (T::NextSessionRotation::average_session_length() / 2_u32.into()) + .try_into() + .unwrap_or(64_u64), + ) + .build() + } + } +} + +impl<T: Config> Pallet<T> { + /// Returns the phase of the current session. + fn session_phase() -> SessionPhase { + let block_in_phase = frame_system::Pallet::<T>::block_number() + .saturating_sub(CurrentSessionStartBlock::<T>::get()); + let Some(block_in_phase) = block_in_phase.checked_sub(&T::NumCoverToCurrentBlocks::get()) + else { + return SessionPhase::CoverToCurrent + }; + let Some(block_in_phase) = + block_in_phase.checked_sub(&T::NumRequestsToCurrentBlocks::get()) + else { + return SessionPhase::RequestsToCurrent + }; + if block_in_phase < T::NumCoverToPrevBlocks::get() { + SessionPhase::CoverToPrev + } else { + SessionPhase::DisconnectFromPrev + } + } + + /// Returns the index and phase of the current session. + pub fn session_status() -> SessionStatus { + SessionStatus { + current_index: CurrentSessionIndex::<T>::get(), + phase: Self::session_phase(), + } + } + + /// Returns the mixnode set for the given session (which should be either the previous or the + /// current session). + fn mixnodes(session_index: SessionIndex) -> Result<Vec<Mixnode>, MixnodesErr> { + let mixnodes: Vec<_> = + Mixnodes::<T>::iter_prefix_values(session_index).map(Into::into).collect(); + if mixnodes.len() < T::MinMixnodes::get() as usize { + Err(MixnodesErr::InsufficientRegistrations { + num: mixnodes.len() as u32, + min: T::MinMixnodes::get(), + }) + } else { + Ok(mixnodes) + } + } + + /// Returns the mixnode set for the previous session. + pub fn prev_mixnodes() -> Result<Vec<Mixnode>, MixnodesErr> { + let Some(prev_session_index) = CurrentSessionIndex::<T>::get().checked_sub(1) else { + return Err(MixnodesErr::InsufficientRegistrations { + num: 0, + min: T::MinMixnodes::get(), + }) + }; + Self::mixnodes(prev_session_index) + } + + /// Returns the mixnode set for the current session. + pub fn current_mixnodes() -> Result<Vec<Mixnode>, MixnodesErr> { + Self::mixnodes(CurrentSessionIndex::<T>::get()) + } + + /// Is now a good time to register, considering only session progress? + fn should_register_by_session_progress( + block_number: BlockNumberFor<T>, + mixnode: &Mixnode, + ) -> bool { + // At the start of each session there are some "slack" blocks during which we avoid + // registering + let block_in_session = block_number.saturating_sub(CurrentSessionStartBlock::<T>::get()); + if block_in_session < T::NumRegisterStartSlackBlocks::get() { + return false + } + + let (Some(end_block), _weight) = + T::NextSessionRotation::estimate_next_session_rotation(block_number) + else { + // Things aren't going to work terribly well in this case as all the authorities will + // just pile in after the slack period... + return true + }; + + let remaining_blocks = end_block + .saturating_sub(block_number) + .saturating_sub(T::NumRegisterEndSlackBlocks::get()); + if remaining_blocks.is_zero() { + // Into the slack time at the end of the session. Not necessarily too late; + // registrations are accepted right up until the session ends. + return true + } + + // Want uniform distribution over the remaining blocks, so pick this block with probability + // 1/remaining_blocks. maybe_register may be called multiple times per block; ensure the + // same decision gets made each time by using a hash of the block number and the mixnode's + // public key as the "random" source. This is slightly biased as remaining_blocks most + // likely won't divide into 2^64, but it doesn't really matter... + let random = twox(block_number, &mixnode.kx_public); + (random % remaining_blocks.try_into().unwrap_or(u64::MAX)) == 0 + } + + fn next_local_authority() -> Option<(AuthorityIndex, AuthorityId)> { + // In the case where multiple local IDs are in the next authority set, we just return the + // first one. There's (currently at least) no point in registering multiple times. + let mut local_ids = AuthorityId::all(); + local_ids.sort(); + NextAuthorityIds::<T>::iter().find(|(_index, id)| local_ids.binary_search(id).is_ok()) + } + + /// `session_index` should be the index of the current session. `authority_index` is the + /// authority index in the _next_ session. + fn already_registered(session_index: SessionIndex, authority_index: AuthorityIndex) -> bool { + Mixnodes::<T>::contains_key(session_index + 1, authority_index) + } + + /// Try to register a mixnode for the next session. + /// + /// If a registration extrinsic is submitted, `true` is returned. The caller should avoid + /// calling `maybe_register` again for a few blocks, to give the submitted extrinsic a chance + /// to get included. + /// + /// With the above exception, `maybe_register` is designed to be called every block. Most of + /// the time it will not do anything, for example: + /// + /// - If it is not an appropriate time to submit a registration extrinsic. + /// - If the local node has already registered a mixnode for the next session. + /// - If the local node is not permitted to register a mixnode for the next session. + /// + /// `session_index` should match `session_status().current_index`; if it does not, `false` is + /// returned immediately. + pub fn maybe_register(session_index: SessionIndex, mixnode: Mixnode) -> bool { + let current_session_index = CurrentSessionIndex::<T>::get(); + if session_index != current_session_index { + log::trace!( + target: LOG_TARGET, + "Session {session_index} registration attempted, \ + but current session is {current_session_index}", + ); + return false + } + + let block_number = frame_system::Pallet::<T>::block_number(); + if !Self::should_register_by_session_progress(block_number, &mixnode) { + log::trace!( + target: LOG_TARGET, + "Waiting for the session to progress further before registering", + ); + return false + } + + let Some((authority_index, authority_id)) = Self::next_local_authority() else { + log::trace!( + target: LOG_TARGET, + "Not an authority in the next session; cannot register a mixnode", + ); + return false + }; + + if Self::already_registered(session_index, authority_index) { + log::trace!( + target: LOG_TARGET, + "Already registered a mixnode for the next session", + ); + return false + } + + let registration = + Registration { block_number, session_index, authority_index, mixnode: mixnode.into() }; + let Some(signature) = authority_id.sign(®istration.encode()) else { + log::debug!(target: LOG_TARGET, "Failed to sign registration"); + return false + }; + let call = Call::register { registration, signature }; + match SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into()) { + Ok(()) => true, + Err(()) => { + log::debug!( + target: LOG_TARGET, + "Failed to submit registration transaction", + ); + false + }, + } + } +} + +impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> { + type Public = AuthorityId; +} + +impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> { + type Key = AuthorityId; + + fn on_genesis_session<'a, I: 'a>(validators: I) + where + I: Iterator<Item = (&'a T::AccountId, Self::Key)>, + { + assert!( + NextAuthorityIds::<T>::iter().next().is_none(), + "Initial authority IDs already set" + ); + for (i, (_, authority_id)) in validators.enumerate() { + NextAuthorityIds::<T>::insert(i as AuthorityIndex, authority_id); + } + } + + fn on_new_session<'a, I: 'a>(changed: bool, _validators: I, queued_validators: I) + where + I: Iterator<Item = (&'a T::AccountId, Self::Key)>, + { + let session_index = CurrentSessionIndex::<T>::mutate(|index| { + *index += 1; + *index + }); + CurrentSessionStartBlock::<T>::put(frame_system::Pallet::<T>::block_number()); + + // Discard the previous previous mixnode set, which we don't need any more + if let Some(prev_prev_session_index) = session_index.checked_sub(2) { + check_removed_all(Mixnodes::<T>::clear_prefix( + prev_prev_session_index, + T::MaxAuthorities::get(), + None, + )); + } + + if changed { + // Save authority set for the next session. Note that we don't care about the authority + // set for the current session; we just care about the key-exchange public keys that + // were registered and are stored in Mixnodes. + check_removed_all(NextAuthorityIds::<T>::clear(T::MaxAuthorities::get(), None)); + for (i, (_, authority_id)) in queued_validators.enumerate() { + NextAuthorityIds::<T>::insert(i as AuthorityIndex, authority_id); + } + } + } + + fn on_disabled(_i: u32) { + // For now, to keep things simple, just ignore + // TODO + } +} diff --git a/substrate/primitives/core/src/crypto.rs b/substrate/primitives/core/src/crypto.rs index be9f56eb2ba4d..e1bfb80046f74 100644 --- a/substrate/primitives/core/src/crypto.rs +++ b/substrate/primitives/core/src/crypto.rs @@ -1161,6 +1161,8 @@ pub mod key_types { pub const STAKING: KeyTypeId = KeyTypeId(*b"stak"); /// A key type for signing statements pub const STATEMENT: KeyTypeId = KeyTypeId(*b"stmt"); + /// Key type for Mixnet module, used to sign key-exchange public keys. Identified as `mixn`. + pub const MIXNET: KeyTypeId = KeyTypeId(*b"mixn"); /// A key type ID useful for tests. pub const DUMMY: KeyTypeId = KeyTypeId(*b"dumy"); } diff --git a/substrate/primitives/mixnet/Cargo.toml b/substrate/primitives/mixnet/Cargo.toml new file mode 100644 index 0000000000000..3e2dcc7ec5c4c --- /dev/null +++ b/substrate/primitives/mixnet/Cargo.toml @@ -0,0 +1,30 @@ +[package] +description = "Substrate mixnet types and runtime interface" +name = "sp-mixnet" +version = "0.1.0-dev" +license = "Apache-2.0" +authors = ["Parity Technologies <admin@parity.io>"] +edition = "2021" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +sp-api = { default-features = false, path = "../api" } +sp-application-crypto = { default-features = false, path = "../application-crypto" } +sp-std = { default-features = false, path = "../std" } + +[features] +default = [ "std" ] +std = [ + "codec/std", + "scale-info/std", + "sp-api/std", + "sp-application-crypto/std", + "sp-std/std", +] diff --git a/substrate/primitives/mixnet/README.md b/substrate/primitives/mixnet/README.md new file mode 100644 index 0000000000000..47c109f6b57c6 --- /dev/null +++ b/substrate/primitives/mixnet/README.md @@ -0,0 +1,3 @@ +Substrate mixnet types and runtime interface. + +License: Apache-2.0 diff --git a/substrate/primitives/mixnet/src/lib.rs b/substrate/primitives/mixnet/src/lib.rs new file mode 100644 index 0000000000000..58b8a10f0cd8d --- /dev/null +++ b/substrate/primitives/mixnet/src/lib.rs @@ -0,0 +1,24 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// 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. + +//! Substrate mixnet types and runtime interface. + +#![warn(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod runtime_api; +pub mod types; diff --git a/substrate/primitives/mixnet/src/runtime_api.rs b/substrate/primitives/mixnet/src/runtime_api.rs new file mode 100644 index 0000000000000..28ab40e633787 --- /dev/null +++ b/substrate/primitives/mixnet/src/runtime_api.rs @@ -0,0 +1,52 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// 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. + +//! Runtime API for querying mixnet configuration and registering mixnodes. + +use super::types::{Mixnode, MixnodesErr, SessionIndex, SessionStatus}; +use sp_std::vec::Vec; + +sp_api::decl_runtime_apis! { + /// API to query the mixnet session status and mixnode sets, and to register mixnodes. + pub trait MixnetApi { + /// Get the index and phase of the current session. + fn session_status() -> SessionStatus; + + /// Get the mixnode set for the previous session. + fn prev_mixnodes() -> Result<Vec<Mixnode>, MixnodesErr>; + + /// Get the mixnode set for the current session. + fn current_mixnodes() -> Result<Vec<Mixnode>, MixnodesErr>; + + /// Try to register a mixnode for the next session. + /// + /// If a registration extrinsic is submitted, `true` is returned. The caller should avoid + /// calling `maybe_register` again for a few blocks, to give the submitted extrinsic a + /// chance to get included. + /// + /// With the above exception, `maybe_register` is designed to be called every block. Most + /// of the time it will not do anything, for example: + /// + /// - If it is not an appropriate time to submit a registration extrinsic. + /// - If the local node has already registered a mixnode for the next session. + /// - If the local node is not permitted to register a mixnode for the next session. + /// + /// `session_index` should match `session_status().current_index`; if it does not, `false` + /// is returned immediately. + fn maybe_register(session_index: SessionIndex, mixnode: Mixnode) -> bool; + } +} diff --git a/substrate/primitives/mixnet/src/types.rs b/substrate/primitives/mixnet/src/types.rs new file mode 100644 index 0000000000000..fc214f94d1cbf --- /dev/null +++ b/substrate/primitives/mixnet/src/types.rs @@ -0,0 +1,100 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// 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. + +//! Mixnet types used by both host and runtime. + +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_std::vec::Vec; + +mod app { + use sp_application_crypto::{app_crypto, key_types::MIXNET, sr25519}; + app_crypto!(sr25519, MIXNET); +} + +/// Authority public session key, used to verify registration signatures. +pub type AuthorityId = app::Public; +/// Authority signature, attached to mixnode registrations. +pub type AuthoritySignature = app::Signature; + +/// Absolute session index. +pub type SessionIndex = u32; + +/// Each session should progress through these phases in order. +#[derive(Decode, Encode, TypeInfo, PartialEq, Eq)] +pub enum SessionPhase { + /// Generate cover traffic to the current session's mixnode set. + CoverToCurrent, + /// Build requests using the current session's mixnode set. + RequestsToCurrent, + /// Only send cover (and forwarded) traffic to the previous session's mixnode set. + CoverToPrev, + /// Disconnect the previous session's mixnode set. + DisconnectFromPrev, +} + +/// The index and phase of the current session. +#[derive(Decode, Encode, TypeInfo)] +pub struct SessionStatus { + /// Index of the current session. + pub current_index: SessionIndex, + /// Current session phase. + pub phase: SessionPhase, +} + +/// Size in bytes of a [`KxPublic`]. +pub const KX_PUBLIC_SIZE: usize = 32; + +/// X25519 public key, used in key exchange between message senders and mixnodes. Mixnode public +/// keys are published on-chain and change every session. Message senders generate a new key for +/// every message they send. +pub type KxPublic = [u8; KX_PUBLIC_SIZE]; + +/// Ed25519 public key of a libp2p peer. +pub type PeerId = [u8; 32]; + +/// Information published on-chain for each mixnode every session. +#[derive(Decode, Encode, TypeInfo)] +pub struct Mixnode { + /// Key-exchange public key for the mixnode. + pub kx_public: KxPublic, + /// libp2p peer ID of the mixnode. + pub peer_id: PeerId, + /// External addresses for the mixnode, in multiaddr format, UTF-8 encoded. + pub external_addresses: Vec<Vec<u8>>, +} + +/// Error querying the runtime for a session's mixnode set. +#[derive(Decode, Encode, TypeInfo)] +pub enum MixnodesErr { + /// Insufficient mixnodes were registered for the session. + InsufficientRegistrations { + /// The number of mixnodes that were registered for the session. + num: u32, + /// The minimum number of mixnodes that must be registered for the mixnet to operate. + min: u32, + }, +} + +impl sp_std::fmt::Display for MixnodesErr { + fn fmt(&self, fmt: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + match self { + MixnodesErr::InsufficientRegistrations { num, min } => + write!(fmt, "{num} mixnode(s) registered; {min} is the minimum"), + } + } +} diff --git a/substrate/scripts/ci/deny.toml b/substrate/scripts/ci/deny.toml index 5297d07143c22..ca059e384a358 100644 --- a/substrate/scripts/ci/deny.toml +++ b/substrate/scripts/ci/deny.toml @@ -68,6 +68,7 @@ exceptions = [ { allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-executor-wasmtime" }, { allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-informant" }, { allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-keystore" }, + { allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-mixnet" }, { allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-network" }, { allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-network-bitswap" }, { allow = ["GPL-3.0 WITH Classpath-exception-2.0"], name = "sc-network-common" }, From 98286ade0b5e8edf1521078c454404c7afaab438 Mon Sep 17 00:00:00 2001 From: georgepisaltu <52418509+georgepisaltu@users.noreply.github.com> Date: Mon, 9 Oct 2023 22:39:12 +0200 Subject: [PATCH 18/54] Fix Asset Hub collator crashing when starting from genesis (#1788) --- cumulus/polkadot-parachain/src/command.rs | 6 +- cumulus/polkadot-parachain/src/service.rs | 155 +++++++++++++++++++++- 2 files changed, 154 insertions(+), 7 deletions(-) diff --git a/cumulus/polkadot-parachain/src/command.rs b/cumulus/polkadot-parachain/src/command.rs index 0e948d24f82da..c47555a32168a 100644 --- a/cumulus/polkadot-parachain/src/command.rs +++ b/cumulus/polkadot-parachain/src/command.rs @@ -836,21 +836,21 @@ pub fn run() -> Result<()> { info!("Is collating: {}", if config.role.is_authority() { "yes" } else { "no" }); match config.chain_spec.runtime() { - Runtime::AssetHubPolkadot => crate::service::start_generic_aura_node::< + Runtime::AssetHubPolkadot => crate::service::start_asset_hub_node::< asset_hub_polkadot_runtime::RuntimeApi, AssetHubPolkadotAuraId, >(config, polkadot_config, collator_options, id, hwbench) .await .map(|r| r.0) .map_err(Into::into), - Runtime::AssetHubKusama => crate::service::start_generic_aura_node::< + Runtime::AssetHubKusama => crate::service::start_asset_hub_node::< asset_hub_kusama_runtime::RuntimeApi, AuraId, >(config, polkadot_config, collator_options, id, hwbench) .await .map(|r| r.0) .map_err(Into::into), - Runtime::AssetHubWestend => crate::service::start_generic_aura_node::< + Runtime::AssetHubWestend => crate::service::start_asset_hub_node::< asset_hub_westend_runtime::RuntimeApi, AuraId, >(config, polkadot_config, collator_options, id, hwbench) diff --git a/cumulus/polkadot-parachain/src/service.rs b/cumulus/polkadot-parachain/src/service.rs index 2f86f54f12a1c..fa61f534784e0 100644 --- a/cumulus/polkadot-parachain/src/service.rs +++ b/cumulus/polkadot-parachain/src/service.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Cumulus. If not, see <http://www.gnu.org/licenses/>. -use codec::Codec; +use codec::{Codec, Decode}; use cumulus_client_cli::CollatorOptions; use cumulus_client_collator::service::CollatorService; use cumulus_client_consensus_aura::collators::{ @@ -44,7 +44,7 @@ use crate::rpc; pub use parachains_common::{AccountId, Balance, Block, BlockNumber, Hash, Header, Nonce}; use cumulus_client_consensus_relay_chain::Verifier as RelayChainVerifier; -use futures::lock::Mutex; +use futures::{lock::Mutex, prelude::*}; use sc_consensus::{ import_queue::{BasicQueue, Verifier as VerifierT}, BlockImportParams, ImportQueue, @@ -54,10 +54,14 @@ use sc_network::{config::FullNetworkConfiguration, NetworkBlock}; use sc_network_sync::SyncingService; use sc_service::{Configuration, PartialComponents, TFullBackend, TFullClient, TaskManager}; use sc_telemetry::{Telemetry, TelemetryHandle, TelemetryWorker, TelemetryWorkerHandle}; -use sp_api::{ApiExt, ConstructRuntimeApi}; +use sp_api::{ApiExt, ConstructRuntimeApi, ProvideRuntimeApi}; use sp_consensus_aura::AuraApi; +use sp_core::traits::SpawnEssentialNamed; use sp_keystore::KeystorePtr; -use sp_runtime::{app_crypto::AppCrypto, traits::Header as HeaderT}; +use sp_runtime::{ + app_crypto::AppCrypto, + traits::{Block as BlockT, Header as HeaderT}, +}; use std::{marker::PhantomData, sync::Arc, time::Duration}; use substrate_prometheus_endpoint::Registry; @@ -1389,6 +1393,149 @@ where .await } +/// Start a shell node which should later transition into an Aura powered parachain node. Asset Hub +/// uses this because at genesis, Asset Hub was on the `shell` runtime which didn't have Aura and +/// needs to sync and upgrade before it can run `AuraApi` functions. +pub async fn start_asset_hub_node<RuntimeApi, AuraId: AppCrypto + Send + Codec + Sync>( + parachain_config: Configuration, + polkadot_config: Configuration, + collator_options: CollatorOptions, + para_id: ParaId, + hwbench: Option<sc_sysinfo::HwBench>, +) -> sc_service::error::Result<(TaskManager, Arc<ParachainClient<RuntimeApi>>)> +where + RuntimeApi: ConstructRuntimeApi<Block, ParachainClient<RuntimeApi>> + Send + Sync + 'static, + RuntimeApi::RuntimeApi: sp_transaction_pool::runtime_api::TaggedTransactionQueue<Block> + + sp_api::Metadata<Block> + + sp_session::SessionKeys<Block> + + sp_api::ApiExt<Block> + + sp_offchain::OffchainWorkerApi<Block> + + sp_block_builder::BlockBuilder<Block> + + cumulus_primitives_core::CollectCollationInfo<Block> + + sp_consensus_aura::AuraApi<Block, <<AuraId as AppCrypto>::Pair as Pair>::Public> + + pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi<Block, Balance> + + frame_rpc_system::AccountNonceApi<Block, AccountId, Nonce>, + <<AuraId as AppCrypto>::Pair as Pair>::Signature: + TryFrom<Vec<u8>> + std::hash::Hash + sp_runtime::traits::Member + Codec, +{ + start_node_impl::<RuntimeApi, _, _, _>( + parachain_config, + polkadot_config, + collator_options, + CollatorSybilResistance::Resistant, // Aura + para_id, + |_| Ok(RpcModule::new(())), + aura_build_import_queue::<_, AuraId>, + |client, + block_import, + prometheus_registry, + telemetry, + task_manager, + relay_chain_interface, + transaction_pool, + sync_oracle, + keystore, + relay_chain_slot_duration, + para_id, + collator_key, + overseer_handle, + announce_block| { + let relay_chain_interface2 = relay_chain_interface.clone(); + + let collator_service = CollatorService::new( + client.clone(), + Arc::new(task_manager.spawn_handle()), + announce_block, + client.clone(), + ); + + let spawner = task_manager.spawn_handle(); + + let proposer_factory = sc_basic_authorship::ProposerFactory::with_proof_recording( + spawner, + client.clone(), + transaction_pool, + prometheus_registry, + telemetry.clone(), + ); + + let collation_future = Box::pin(async move { + // Start collating with the `shell` runtime while waiting for an upgrade to an Aura + // compatible runtime. + let mut request_stream = cumulus_client_collator::relay_chain_driven::init( + collator_key.clone(), + para_id, + overseer_handle.clone(), + ) + .await; + while let Some(request) = request_stream.next().await { + let pvd = request.persisted_validation_data().clone(); + let last_head_hash = + match <Block as BlockT>::Header::decode(&mut &pvd.parent_head.0[..]) { + Ok(header) => header.hash(), + Err(e) => { + log::error!("Could not decode the head data: {e}"); + request.complete(None); + continue + }, + }; + + // Check if we have upgraded to an Aura compatible runtime and transition if + // necessary. + if client + .runtime_api() + .has_api::<dyn AuraApi<Block, AuraId>>(last_head_hash) + .unwrap_or(false) + { + // Respond to this request before transitioning to Aura. + request.complete(None); + break + } + } + + // Move to Aura consensus. + let slot_duration = match cumulus_client_consensus_aura::slot_duration(&*client) { + Ok(d) => d, + Err(e) => { + log::error!("Could not get Aura slot duration: {e}"); + return + }, + }; + + let proposer = Proposer::new(proposer_factory); + + let params = BasicAuraParams { + create_inherent_data_providers: move |_, ()| async move { Ok(()) }, + block_import, + para_client: client, + relay_client: relay_chain_interface2, + sync_oracle, + keystore, + collator_key, + para_id, + overseer_handle, + slot_duration, + relay_chain_slot_duration, + proposer, + collator_service, + // Very limited proposal time. + authoring_duration: Duration::from_millis(500), + }; + + basic_aura::run::<Block, <AuraId as AppCrypto>::Pair, _, _, _, _, _, _, _>(params) + .await + }); + + let spawner = task_manager.spawn_essential_handle(); + spawner.spawn_essential("cumulus-asset-hub-collator", None, collation_future); + + Ok(()) + }, + hwbench, + ) + .await +} + /// Start an aura powered parachain node which uses the lookahead collator to support async backing. /// This node is basic in the sense that its runtime api doesn't include common contents such as /// transaction payment. Used for aura glutton. From 93d9c8c24e5d470e036caaa23df08dcf0c527dd6 Mon Sep 17 00:00:00 2001 From: David Emett <david.emett@parity.io> Date: Tue, 10 Oct 2023 09:14:56 +0200 Subject: [PATCH 19/54] Make CheckNonce refuse transactions signed by accounts with no providers (#1578) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See #1453. Co-authored-by: Bastian Köcher <git@kchr.de> --- .../node/executor/tests/submit_transaction.rs | 2 +- .../client/service/test/src/client/mod.rs | 25 +++------ .../system/src/extensions/check_nonce.rs | 56 ++++++++++++++++++- 3 files changed, 62 insertions(+), 21 deletions(-) diff --git a/substrate/bin/node/executor/tests/submit_transaction.rs b/substrate/bin/node/executor/tests/submit_transaction.rs index 7678a3c6e5a9f..5cbb0103d471b 100644 --- a/substrate/bin/node/executor/tests/submit_transaction.rs +++ b/substrate/bin/node/executor/tests/submit_transaction.rs @@ -239,7 +239,7 @@ fn submitted_transaction_should_be_valid() { let author = extrinsic.signature.clone().unwrap().0; let address = Indices::lookup(author).unwrap(); let data = pallet_balances::AccountData { free: 5_000_000_000_000, ..Default::default() }; - let account = frame_system::AccountInfo { data, ..Default::default() }; + let account = frame_system::AccountInfo { providers: 1, data, ..Default::default() }; <frame_system::Account<Runtime>>::insert(&address, account); // check validity diff --git a/substrate/client/service/test/src/client/mod.rs b/substrate/client/service/test/src/client/mod.rs index c40ac33da4bb9..f82008755874b 100644 --- a/substrate/client/service/test/src/client/mod.rs +++ b/substrate/client/service/test/src/client/mod.rs @@ -39,7 +39,6 @@ use sp_runtime::{ }; use sp_state_machine::{backend::Backend as _, InMemoryBackend, OverlayedChanges, StateMachine}; use sp_storage::{ChildInfo, StorageKey}; -use sp_trie::{LayoutV0, TrieConfiguration}; use std::{collections::HashSet, sync::Arc}; use substrate_test_runtime::TestAPI; use substrate_test_runtime_client::{ @@ -62,22 +61,17 @@ fn construct_block( backend: &InMemoryBackend<BlakeTwo256>, number: BlockNumber, parent_hash: Hash, - state_root: Hash, txs: Vec<Transfer>, -) -> (Vec<u8>, Hash) { +) -> Vec<u8> { let transactions = txs.into_iter().map(|tx| tx.into_unchecked_extrinsic()).collect::<Vec<_>>(); - let iter = transactions.iter().map(Encode::encode); - let extrinsics_root = LayoutV0::<BlakeTwo256>::ordered_trie_root(iter).into(); - let mut header = Header { parent_hash, number, - state_root, - extrinsics_root, + state_root: Default::default(), + extrinsics_root: Default::default(), digest: Digest { logs: vec![] }, }; - let hash = header.hash(); let mut overlay = OverlayedChanges::default(); let backend_runtime_code = sp_state_machine::backend::BackendRuntimeCode::new(backend); let runtime_code = backend_runtime_code.runtime_code().expect("Code is part of the backend"); @@ -124,19 +118,16 @@ fn construct_block( .unwrap(); header = Header::decode(&mut &ret_data[..]).unwrap(); - (vec![].and(&Block { header, extrinsics: transactions }), hash) + vec![].and(&Block { header, extrinsics: transactions }) } -fn block1(genesis_hash: Hash, backend: &InMemoryBackend<BlakeTwo256>) -> (Vec<u8>, Hash) { +fn block1(genesis_hash: Hash, backend: &InMemoryBackend<BlakeTwo256>) -> Vec<u8> { construct_block( backend, 1, genesis_hash, - array_bytes::hex_n_into_unchecked( - "25e5b37074063ab75c889326246640729b40d0c86932edc527bc80db0e04fe5c", - ), vec![Transfer { - from: AccountKeyring::Alice.into(), + from: AccountKeyring::One.into(), to: AccountKeyring::Two.into(), amount: 69 * DOLLARS, nonce: 0, @@ -175,7 +166,7 @@ fn construct_genesis_should_work_with_native() { let genesis_hash = insert_genesis_block(&mut storage); let backend = InMemoryBackend::from((storage, StateVersion::default())); - let (b1data, _b1hash) = block1(genesis_hash, &backend); + let b1data = block1(genesis_hash, &backend); let backend_runtime_code = sp_state_machine::backend::BackendRuntimeCode::new(&backend); let runtime_code = backend_runtime_code.runtime_code().expect("Code is part of the backend"); @@ -206,7 +197,7 @@ fn construct_genesis_should_work_with_wasm() { let genesis_hash = insert_genesis_block(&mut storage); let backend = InMemoryBackend::from((storage, StateVersion::default())); - let (b1data, _b1hash) = block1(genesis_hash, &backend); + let b1data = block1(genesis_hash, &backend); let backend_runtime_code = sp_state_machine::backend::BackendRuntimeCode::new(&backend); let runtime_code = backend_runtime_code.runtime_code().expect("Code is part of the backend"); diff --git a/substrate/frame/system/src/extensions/check_nonce.rs b/substrate/frame/system/src/extensions/check_nonce.rs index 2939fd6534c09..7504a814aef13 100644 --- a/substrate/frame/system/src/extensions/check_nonce.rs +++ b/substrate/frame/system/src/extensions/check_nonce.rs @@ -20,7 +20,7 @@ use codec::{Decode, Encode}; use frame_support::dispatch::DispatchInfo; use scale_info::TypeInfo; use sp_runtime::{ - traits::{DispatchInfoOf, Dispatchable, One, SignedExtension}, + traits::{DispatchInfoOf, Dispatchable, One, SignedExtension, Zero}, transaction_validity::{ InvalidTransaction, TransactionLongevity, TransactionValidity, TransactionValidityError, ValidTransaction, @@ -80,6 +80,10 @@ where _len: usize, ) -> Result<(), TransactionValidityError> { let mut account = crate::Account::<T>::get(who); + if account.providers.is_zero() && account.sufficients.is_zero() { + // Nonce storage not paid for + return Err(InvalidTransaction::Payment.into()) + } if self.0 != account.nonce { return Err(if self.0 < account.nonce { InvalidTransaction::Stale @@ -100,8 +104,11 @@ where _info: &DispatchInfoOf<Self::Call>, _len: usize, ) -> TransactionValidity { - // check index let account = crate::Account::<T>::get(who); + if account.providers.is_zero() && account.sufficients.is_zero() { + // Nonce storage not paid for + return InvalidTransaction::Payment.into() + } if self.0 < account.nonce { return InvalidTransaction::Stale.into() } @@ -137,7 +144,7 @@ mod tests { crate::AccountInfo { nonce: 1, consumers: 0, - providers: 0, + providers: 1, sufficients: 0, data: 0, }, @@ -164,4 +171,47 @@ mod tests { ); }) } + + #[test] + fn signed_ext_check_nonce_requires_provider() { + new_test_ext().execute_with(|| { + crate::Account::<Test>::insert( + 2, + crate::AccountInfo { + nonce: 1, + consumers: 0, + providers: 1, + sufficients: 0, + data: 0, + }, + ); + crate::Account::<Test>::insert( + 3, + crate::AccountInfo { + nonce: 1, + consumers: 0, + providers: 0, + sufficients: 1, + data: 0, + }, + ); + let info = DispatchInfo::default(); + let len = 0_usize; + // Both providers and sufficients zero + assert_noop!( + CheckNonce::<Test>(1).validate(&1, CALL, &info, len), + InvalidTransaction::Payment + ); + assert_noop!( + CheckNonce::<Test>(1).pre_dispatch(&1, CALL, &info, len), + InvalidTransaction::Payment + ); + // Non-zero providers + assert_ok!(CheckNonce::<Test>(1).validate(&2, CALL, &info, len)); + assert_ok!(CheckNonce::<Test>(1).pre_dispatch(&2, CALL, &info, len)); + // Non-zero sufficients + assert_ok!(CheckNonce::<Test>(1).validate(&3, CALL, &info, len)); + assert_ok!(CheckNonce::<Test>(1).pre_dispatch(&3, CALL, &info, len)); + }) + } } From 2b4b33d01f22b1f4037a404597357f398e21224f Mon Sep 17 00:00:00 2001 From: Rahul Subramaniyam <78006270+rahulksnv@users.noreply.github.com> Date: Tue, 10 Oct 2023 02:46:23 -0700 Subject: [PATCH 20/54] Check for parent of first ready block being on chain (#1812) When retrieving the ready blocks, verify that the parent of the first ready block is on chain. If the parent is not on chain, we are downloading from a fork. In this case, keep downloading until we have a parent on chain (common ancestor). Resolves https://github.com/paritytech/polkadot-sdk/issues/493. --------- Co-authored-by: Aaro Altonen <48052676+altonen@users.noreply.github.com> --- substrate/client/network/sync/src/blocks.rs | 25 ++ substrate/client/network/sync/src/lib.rs | 397 +++++++++++++++++++- 2 files changed, 421 insertions(+), 1 deletion(-) diff --git a/substrate/client/network/sync/src/blocks.rs b/substrate/client/network/sync/src/blocks.rs index 240c1ca1f8b26..cad50fef3e321 100644 --- a/substrate/client/network/sync/src/blocks.rs +++ b/substrate/client/network/sync/src/blocks.rs @@ -212,6 +212,31 @@ impl<B: BlockT> BlockCollection<B> { ready } + /// Returns the block header of the first block that is ready for importing. + /// `from` is the maximum block number for the start of the range that we are interested in. + /// The function will return None if the first block ready is higher than `from`. + /// The logic is structured to be consistent with ready_blocks(). + pub fn first_ready_block_header(&self, from: NumberFor<B>) -> Option<B::Header> { + let mut prev = from; + for (&start, range_data) in &self.blocks { + if start > prev { + break + } + + match range_data { + BlockRangeState::Complete(blocks) => { + let len = (blocks.len() as u32).into(); + prev = start + len; + if let Some(BlockData { block, .. }) = blocks.first() { + return block.header.clone() + } + }, + _ => continue, + } + } + None + } + pub fn clear_queued(&mut self, hash: &B::Hash) { if let Some((from, to)) = self.queued_blocks.remove(hash) { let mut block_num = from; diff --git a/substrate/client/network/sync/src/lib.rs b/substrate/client/network/sync/src/lib.rs index 10eaa2450518e..a291da4a90d58 100644 --- a/substrate/client/network/sync/src/lib.rs +++ b/substrate/client/network/sync/src/lib.rs @@ -1405,8 +1405,27 @@ where /// Get the set of downloaded blocks that are ready to be queued for import. fn ready_blocks(&mut self) -> Vec<IncomingBlock<B>> { + let start_block = self.best_queued_number + One::one(); + + // Verify that the parent of the first available block is in the chain. + // If not, we are downloading from a fork. In this case, wait until + // the start block has a parent on chain. + let parent_on_chain = + self.blocks.first_ready_block_header(start_block).map_or(false, |hdr| { + std::matches!( + self.block_status(hdr.parent_hash()).unwrap_or(BlockStatus::Unknown), + BlockStatus::InChainWithState | + BlockStatus::InChainPruned | + BlockStatus::Queued + ) + }); + + if !parent_on_chain { + return vec![] + } + self.blocks - .ready_blocks(self.best_queued_number + One::one()) + .ready_blocks(start_block) .into_iter() .map(|block_data| { let justifications = block_data @@ -3364,4 +3383,380 @@ mod test { pending_responses.remove(&peers[1]); assert_eq!(pending_responses.len(), 0); } + + #[test] + fn syncs_fork_with_partial_response_extends_tip() { + sp_tracing::try_init_simple(); + + // Set up: the two chains share the first 15 blocks before + // diverging. The other(canonical) chain fork is longer. + let max_blocks_per_request = 64; + let common_ancestor = 15; + let non_canonical_chain_length = common_ancestor + 3; + let canonical_chain_length = common_ancestor + max_blocks_per_request + 10; + + let (_chain_sync_network_provider, chain_sync_network_handle) = + NetworkServiceProvider::new(); + let mut client = Arc::new(TestClientBuilder::new().build()); + + // Blocks on the non-canonical chain. + let non_canonical_blocks = (0..non_canonical_chain_length) + .map(|_| build_block(&mut client, None, false)) + .collect::<Vec<_>>(); + + // Blocks on the canonical chain. + let canonical_blocks = { + let mut client = Arc::new(TestClientBuilder::new().build()); + let common_blocks = non_canonical_blocks[..common_ancestor as usize] + .into_iter() + .inspect(|b| block_on(client.import(BlockOrigin::Own, (*b).clone())).unwrap()) + .cloned() + .collect::<Vec<_>>(); + + common_blocks + .into_iter() + .chain( + (0..(canonical_chain_length - common_ancestor as u32)) + .map(|_| build_block(&mut client, None, true)), + ) + .collect::<Vec<_>>() + }; + + let mut sync = ChainSync::new( + SyncMode::Full, + client.clone(), + ProtocolName::from("test-block-announce-protocol"), + 1, + max_blocks_per_request, + None, + chain_sync_network_handle, + ) + .unwrap(); + + // Connect the node we will sync from + let peer_id = PeerId::random(); + let canonical_tip = canonical_blocks.last().unwrap().clone(); + let mut request = sync + .new_peer(peer_id, canonical_tip.hash(), *canonical_tip.header().number()) + .unwrap() + .unwrap(); + assert_eq!(FromBlock::Number(client.info().best_number), request.from); + assert_eq!(Some(1), request.max); + + // Do the ancestor search + loop { + let block = + &canonical_blocks[unwrap_from_block_number(request.from.clone()) as usize - 1]; + let response = create_block_response(vec![block.clone()]); + + let on_block_data = sync.on_block_data(&peer_id, Some(request), response).unwrap(); + request = if let OnBlockData::Request(_peer, request) = on_block_data { + request + } else { + // We found the ancestor + break + }; + + log::trace!(target: LOG_TARGET, "Request: {request:?}"); + } + + // The response for the 64 blocks is returned in two parts: + // part 1: last 61 blocks [19..79], part 2: first 3 blocks [16-18]. + // Even though the first part extends the current chain ending at 18, + // it should not result in an import yet. + let resp_1_from = common_ancestor as u64 + max_blocks_per_request as u64; + let resp_2_from = common_ancestor as u64 + 3; + + // No import expected. + let request = get_block_request( + &mut sync, + FromBlock::Number(resp_1_from), + max_blocks_per_request as u32, + &peer_id, + ); + + let from = unwrap_from_block_number(request.from.clone()); + let mut resp_blocks = canonical_blocks[18..from as usize].to_vec(); + resp_blocks.reverse(); + let response = create_block_response(resp_blocks.clone()); + let res = sync.on_block_data(&peer_id, Some(request), response).unwrap(); + assert!(matches!( + res, + OnBlockData::Import(ImportBlocksAction{ origin: _, blocks }) if blocks.is_empty() + ),); + + // Gap filled, expect max_blocks_per_request being imported now. + let request = get_block_request(&mut sync, FromBlock::Number(resp_2_from), 3, &peer_id); + let mut resp_blocks = canonical_blocks[common_ancestor as usize..18].to_vec(); + resp_blocks.reverse(); + let response = create_block_response(resp_blocks.clone()); + let res = sync.on_block_data(&peer_id, Some(request), response).unwrap(); + let to_import: Vec<_> = match &res { + OnBlockData::Import(ImportBlocksAction { origin: _, blocks }) => { + assert_eq!(blocks.len(), sync.max_blocks_per_request as usize); + blocks + .iter() + .map(|b| { + let num = *b.header.as_ref().unwrap().number() as usize; + canonical_blocks[num - 1].clone() + }) + .collect() + }, + _ => { + panic!("Unexpected response: {res:?}"); + }, + }; + + let _ = sync.on_blocks_processed( + max_blocks_per_request as usize, + resp_blocks.len(), + resp_blocks + .iter() + .rev() + .map(|b| { + ( + Ok(BlockImportStatus::ImportedUnknown( + *b.header().number(), + Default::default(), + Some(peer_id), + )), + b.hash(), + ) + }) + .collect(), + ); + to_import.into_iter().for_each(|b| { + assert!(matches!(client.block(*b.header.parent_hash()), Ok(Some(_)))); + block_on(client.import(BlockOrigin::Own, b)).unwrap(); + }); + let expected_number = common_ancestor as u32 + max_blocks_per_request as u32; + assert_eq!(sync.best_queued_number as u32, expected_number); + assert_eq!(sync.best_queued_hash, canonical_blocks[expected_number as usize - 1].hash()); + // Sync rest of the chain. + let request = + get_block_request(&mut sync, FromBlock::Hash(canonical_tip.hash()), 10_u32, &peer_id); + let mut resp_blocks = canonical_blocks + [(canonical_chain_length - 10) as usize..canonical_chain_length as usize] + .to_vec(); + resp_blocks.reverse(); + let response = create_block_response(resp_blocks.clone()); + let res = sync.on_block_data(&peer_id, Some(request), response).unwrap(); + assert!(matches!( + res, + OnBlockData::Import(ImportBlocksAction{ origin: _, blocks }) if blocks.len() == 10 as usize + ),); + let _ = sync.on_blocks_processed( + max_blocks_per_request as usize, + resp_blocks.len(), + resp_blocks + .iter() + .rev() + .map(|b| { + ( + Ok(BlockImportStatus::ImportedUnknown( + *b.header().number(), + Default::default(), + Some(peer_id), + )), + b.hash(), + ) + }) + .collect(), + ); + resp_blocks.into_iter().rev().for_each(|b| { + assert!(matches!(client.block(*b.header.parent_hash()), Ok(Some(_)))); + block_on(client.import(BlockOrigin::Own, b)).unwrap(); + }); + let expected_number = canonical_chain_length as u32; + assert_eq!(sync.best_queued_number as u32, expected_number); + assert_eq!(sync.best_queued_hash, canonical_blocks[expected_number as usize - 1].hash()); + } + + #[test] + fn syncs_fork_with_partial_response_does_not_extend_tip() { + sp_tracing::try_init_simple(); + + // Set up: the two chains share the first 15 blocks before + // diverging. The other(canonical) chain fork is longer. + let max_blocks_per_request = 64; + let common_ancestor = 15; + let non_canonical_chain_length = common_ancestor + 3; + let canonical_chain_length = common_ancestor + max_blocks_per_request + 10; + + let (_chain_sync_network_provider, chain_sync_network_handle) = + NetworkServiceProvider::new(); + let mut client = Arc::new(TestClientBuilder::new().build()); + + // Blocks on the non-canonical chain. + let non_canonical_blocks = (0..non_canonical_chain_length) + .map(|_| build_block(&mut client, None, false)) + .collect::<Vec<_>>(); + + // Blocks on the canonical chain. + let canonical_blocks = { + let mut client = Arc::new(TestClientBuilder::new().build()); + let common_blocks = non_canonical_blocks[..common_ancestor as usize] + .into_iter() + .inspect(|b| block_on(client.import(BlockOrigin::Own, (*b).clone())).unwrap()) + .cloned() + .collect::<Vec<_>>(); + + common_blocks + .into_iter() + .chain( + (0..(canonical_chain_length - common_ancestor as u32)) + .map(|_| build_block(&mut client, None, true)), + ) + .collect::<Vec<_>>() + }; + + let mut sync = ChainSync::new( + SyncMode::Full, + client.clone(), + ProtocolName::from("test-block-announce-protocol"), + 1, + max_blocks_per_request, + None, + chain_sync_network_handle, + ) + .unwrap(); + + // Connect the node we will sync from + let peer_id = PeerId::random(); + let canonical_tip = canonical_blocks.last().unwrap().clone(); + let mut request = sync + .new_peer(peer_id, canonical_tip.hash(), *canonical_tip.header().number()) + .unwrap() + .unwrap(); + assert_eq!(FromBlock::Number(client.info().best_number), request.from); + assert_eq!(Some(1), request.max); + + // Do the ancestor search + loop { + let block = + &canonical_blocks[unwrap_from_block_number(request.from.clone()) as usize - 1]; + let response = create_block_response(vec![block.clone()]); + + let on_block_data = sync.on_block_data(&peer_id, Some(request), response).unwrap(); + request = if let OnBlockData::Request(_peer, request) = on_block_data { + request + } else { + // We found the ancestor + break + }; + + log::trace!(target: LOG_TARGET, "Request: {request:?}"); + } + + // The response for the 64 blocks is returned in two parts: + // part 1: last 62 blocks [18..79], part 2: first 2 blocks [16-17]. + // Even though the first part extends the current chain ending at 18, + // it should not result in an import yet. + let resp_1_from = common_ancestor as u64 + max_blocks_per_request as u64; + let resp_2_from = common_ancestor as u64 + 2; + + // No import expected. + let request = get_block_request( + &mut sync, + FromBlock::Number(resp_1_from), + max_blocks_per_request as u32, + &peer_id, + ); + + let from = unwrap_from_block_number(request.from.clone()); + let mut resp_blocks = canonical_blocks[17..from as usize].to_vec(); + resp_blocks.reverse(); + let response = create_block_response(resp_blocks.clone()); + let res = sync.on_block_data(&peer_id, Some(request), response).unwrap(); + assert!(matches!( + res, + OnBlockData::Import(ImportBlocksAction{ origin: _, blocks }) if blocks.is_empty() + ),); + + // Gap filled, expect max_blocks_per_request being imported now. + let request = get_block_request(&mut sync, FromBlock::Number(resp_2_from), 2, &peer_id); + let mut resp_blocks = canonical_blocks[common_ancestor as usize..17].to_vec(); + resp_blocks.reverse(); + let response = create_block_response(resp_blocks.clone()); + let res = sync.on_block_data(&peer_id, Some(request), response).unwrap(); + let to_import: Vec<_> = match &res { + OnBlockData::Import(ImportBlocksAction { origin: _, blocks }) => { + assert_eq!(blocks.len(), sync.max_blocks_per_request as usize); + blocks + .iter() + .map(|b| { + let num = *b.header.as_ref().unwrap().number() as usize; + canonical_blocks[num - 1].clone() + }) + .collect() + }, + _ => { + panic!("Unexpected response: {res:?}"); + }, + }; + + let _ = sync.on_blocks_processed( + max_blocks_per_request as usize, + resp_blocks.len(), + resp_blocks + .iter() + .rev() + .map(|b| { + ( + Ok(BlockImportStatus::ImportedUnknown( + *b.header().number(), + Default::default(), + Some(peer_id), + )), + b.hash(), + ) + }) + .collect(), + ); + to_import.into_iter().for_each(|b| { + assert!(matches!(client.block(*b.header.parent_hash()), Ok(Some(_)))); + block_on(client.import(BlockOrigin::Own, b)).unwrap(); + }); + let expected_number = common_ancestor as u32 + max_blocks_per_request as u32; + assert_eq!(sync.best_queued_number as u32, expected_number); + assert_eq!(sync.best_queued_hash, canonical_blocks[expected_number as usize - 1].hash()); + // Sync rest of the chain. + let request = + get_block_request(&mut sync, FromBlock::Hash(canonical_tip.hash()), 10_u32, &peer_id); + let mut resp_blocks = canonical_blocks + [(canonical_chain_length - 10) as usize..canonical_chain_length as usize] + .to_vec(); + resp_blocks.reverse(); + let response = create_block_response(resp_blocks.clone()); + let res = sync.on_block_data(&peer_id, Some(request), response).unwrap(); + assert!(matches!( + res, + OnBlockData::Import(ImportBlocksAction{ origin: _, blocks }) if blocks.len() == 10 as usize + ),); + let _ = sync.on_blocks_processed( + max_blocks_per_request as usize, + resp_blocks.len(), + resp_blocks + .iter() + .rev() + .map(|b| { + ( + Ok(BlockImportStatus::ImportedUnknown( + *b.header().number(), + Default::default(), + Some(peer_id), + )), + b.hash(), + ) + }) + .collect(), + ); + resp_blocks.into_iter().rev().for_each(|b| { + assert!(matches!(client.block(*b.header.parent_hash()), Ok(Some(_)))); + block_on(client.import(BlockOrigin::Own, b)).unwrap(); + }); + let expected_number = canonical_chain_length as u32; + assert_eq!(sync.best_queued_number as u32, expected_number); + assert_eq!(sync.best_queued_hash, canonical_blocks[expected_number as usize - 1].hash()); + } } From ebf442336f14f45d0a6522b65e68f567107cfb46 Mon Sep 17 00:00:00 2001 From: Svyatoslav Nikolsky <svyatonik@gmail.com> Date: Tue, 10 Oct 2023 14:20:25 +0300 Subject: [PATCH 21/54] Update bridges subtree (#1803) --- .../runtime-common/src/messages_call_ext.rs | 29 ++++- .../src/refund_relayer_extension.rs | 107 +++++++++++++++++- bridges/modules/grandpa/src/call_ext.rs | 21 +++- bridges/modules/parachains/src/call_ext.rs | 19 +++- 4 files changed, 164 insertions(+), 12 deletions(-) diff --git a/bridges/bin/runtime-common/src/messages_call_ext.rs b/bridges/bin/runtime-common/src/messages_call_ext.rs index 07a99d2c0a16c..5303fcb7ba030 100644 --- a/bridges/bin/runtime-common/src/messages_call_ext.rs +++ b/bridges/bin/runtime-common/src/messages_call_ext.rs @@ -18,6 +18,7 @@ use crate::messages::{ source::FromBridgedChainMessagesDeliveryProof, target::FromBridgedChainMessagesProof, }; use bp_messages::{target_chain::MessageDispatch, InboundLaneData, LaneId, MessageNonce}; +use bp_runtime::OwnedBridgeModule; use frame_support::{ dispatch::CallableCallFor, traits::{Get, IsSubType}, @@ -187,8 +188,22 @@ pub trait MessagesCallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>: /// or a `ReceiveMessagesDeliveryProof` call, if the call is for the provided lane. fn call_info_for(&self, lane_id: LaneId) -> Option<CallInfo>; - /// Check that a `ReceiveMessagesProof` or a `ReceiveMessagesDeliveryProof` call is trying - /// to deliver/confirm at least some messages that are better than the ones we know of. + /// Ensures that a `ReceiveMessagesProof` or a `ReceiveMessagesDeliveryProof` call: + /// + /// - does not deliver already delivered messages. We require all messages in the + /// `ReceiveMessagesProof` call to be undelivered; + /// + /// - does not submit empty `ReceiveMessagesProof` call with zero messages, unless the lane + /// needs to be unblocked by providing relayer rewards proof; + /// + /// - brings no new delivery confirmations in a `ReceiveMessagesDeliveryProof` call. We require + /// at least one new delivery confirmation in the unrewarded relayers set; + /// + /// - does not violate some basic (easy verifiable) messages pallet rules obsolete (like + /// submitting a call when a pallet is halted or delivering messages when a dispatcher is + /// inactive). + /// + /// If one of above rules is violated, the transaction is treated as invalid. fn check_obsolete_call(&self) -> TransactionValidity; } @@ -278,7 +293,17 @@ impl< } fn check_obsolete_call(&self) -> TransactionValidity { + let is_pallet_halted = Pallet::<T, I>::ensure_not_halted().is_err(); match self.call_info() { + Some(proof_info) if is_pallet_halted => { + log::trace!( + target: pallet_bridge_messages::LOG_TARGET, + "Rejecting messages transaction on halted pallet: {:?}", + proof_info + ); + + return sp_runtime::transaction_validity::InvalidTransaction::Call.into() + }, Some(CallInfo::ReceiveMessagesProof(proof_info)) if proof_info.is_obsolete(T::MessageDispatch::is_active()) => { diff --git a/bridges/bin/runtime-common/src/refund_relayer_extension.rs b/bridges/bin/runtime-common/src/refund_relayer_extension.rs index 876c069dc0f77..6d8b211480858 100644 --- a/bridges/bin/runtime-common/src/refund_relayer_extension.rs +++ b/bridges/bin/runtime-common/src/refund_relayer_extension.rs @@ -838,21 +838,23 @@ mod tests { mock::*, }; use bp_messages::{ - DeliveredMessages, InboundLaneData, MessageNonce, OutboundLaneData, UnrewardedRelayer, - UnrewardedRelayersState, + DeliveredMessages, InboundLaneData, MessageNonce, MessagesOperatingMode, OutboundLaneData, + UnrewardedRelayer, UnrewardedRelayersState, }; use bp_parachains::{BestParaHeadHash, ParaInfo}; use bp_polkadot_core::parachains::{ParaHeadsProof, ParaId}; - use bp_runtime::HeaderId; + use bp_runtime::{BasicOperatingMode, HeaderId}; use bp_test_utils::{make_default_justification, test_keyring}; use frame_support::{ assert_storage_noop, parameter_types, traits::{fungible::Mutate, ReservableCurrency}, weights::Weight, }; - use pallet_bridge_grandpa::{Call as GrandpaCall, StoredAuthoritySet}; - use pallet_bridge_messages::Call as MessagesCall; - use pallet_bridge_parachains::{Call as ParachainsCall, RelayBlockHash}; + use pallet_bridge_grandpa::{Call as GrandpaCall, Pallet as GrandpaPallet, StoredAuthoritySet}; + use pallet_bridge_messages::{Call as MessagesCall, Pallet as MessagesPallet}; + use pallet_bridge_parachains::{ + Call as ParachainsCall, Pallet as ParachainsPallet, RelayBlockHash, + }; use sp_runtime::{ traits::{ConstU64, Header as HeaderT}, transaction_validity::{InvalidTransaction, ValidTransaction}, @@ -1592,6 +1594,99 @@ mod tests { }); } + #[test] + fn ext_rejects_batch_with_grandpa_finality_proof_when_grandpa_pallet_is_halted() { + run_test(|| { + initialize_environment(100, 100, 100); + + GrandpaPallet::<TestRuntime, ()>::set_operating_mode( + RuntimeOrigin::root(), + BasicOperatingMode::Halted, + ) + .unwrap(); + + assert_eq!( + run_pre_dispatch(all_finality_and_delivery_batch_call(200, 200, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + ); + assert_eq!( + run_pre_dispatch(all_finality_and_confirmation_batch_call(200, 200, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + ); + }); + } + + #[test] + fn ext_rejects_batch_with_parachain_finality_proof_when_parachains_pallet_is_halted() { + run_test(|| { + initialize_environment(100, 100, 100); + + ParachainsPallet::<TestRuntime, ()>::set_operating_mode( + RuntimeOrigin::root(), + BasicOperatingMode::Halted, + ) + .unwrap(); + + assert_eq!( + run_pre_dispatch(all_finality_and_delivery_batch_call(200, 200, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + ); + assert_eq!( + run_pre_dispatch(all_finality_and_confirmation_batch_call(200, 200, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + ); + + assert_eq!( + run_pre_dispatch(parachain_finality_and_delivery_batch_call(200, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + ); + assert_eq!( + run_pre_dispatch(parachain_finality_and_confirmation_batch_call(200, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + ); + }); + } + + #[test] + fn ext_rejects_transaction_when_messages_pallet_is_halted() { + run_test(|| { + initialize_environment(100, 100, 100); + + MessagesPallet::<TestRuntime, ()>::set_operating_mode( + RuntimeOrigin::root(), + MessagesOperatingMode::Basic(BasicOperatingMode::Halted), + ) + .unwrap(); + + assert_eq!( + run_pre_dispatch(all_finality_and_delivery_batch_call(200, 200, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + ); + assert_eq!( + run_pre_dispatch(all_finality_and_confirmation_batch_call(200, 200, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + ); + + assert_eq!( + run_pre_dispatch(parachain_finality_and_delivery_batch_call(200, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + ); + assert_eq!( + run_pre_dispatch(parachain_finality_and_confirmation_batch_call(200, 200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + ); + + assert_eq!( + run_pre_dispatch(message_delivery_call(200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + ); + assert_eq!( + run_pre_dispatch(message_confirmation_call(200)), + Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + ); + }); + } + #[test] fn pre_dispatch_parses_batch_with_relay_chain_and_parachain_headers() { run_test(|| { diff --git a/bridges/modules/grandpa/src/call_ext.rs b/bridges/modules/grandpa/src/call_ext.rs index e0648d5dd0f1d..f238064f92bca 100644 --- a/bridges/modules/grandpa/src/call_ext.rs +++ b/bridges/modules/grandpa/src/call_ext.rs @@ -16,7 +16,7 @@ use crate::{weights::WeightInfo, BridgedBlockNumber, BridgedHeader, Config, Error, Pallet}; use bp_header_chain::{justification::GrandpaJustification, ChainWithGrandpa}; -use bp_runtime::BlockNumberOf; +use bp_runtime::{BlockNumberOf, OwnedBridgeModule}; use codec::Encode; use frame_support::{dispatch::CallableCallFor, traits::IsSubType, weights::Weight}; use sp_runtime::{ @@ -126,6 +126,10 @@ pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>: _ => return Ok(ValidTransaction::default()), }; + if Pallet::<T, I>::ensure_not_halted().is_err() { + return InvalidTransaction::Call.into() + } + match SubmitFinalityProofHelper::<T, I>::check_obsolete(finality_target.block_number) { Ok(_) => Ok(ValidTransaction::default()), Err(Error::<T, I>::OldHeader) => InvalidTransaction::Stale.into(), @@ -192,10 +196,10 @@ mod tests { use crate::{ call_ext::CallSubType, mock::{run_test, test_header, RuntimeCall, TestBridgedChain, TestNumber, TestRuntime}, - BestFinalized, Config, WeightInfo, + BestFinalized, Config, PalletOperatingMode, WeightInfo, }; use bp_header_chain::ChainWithGrandpa; - use bp_runtime::HeaderId; + use bp_runtime::{BasicOperatingMode, HeaderId}; use bp_test_utils::{ make_default_justification, make_justification_for_header, JustificationGeneratorParams, }; @@ -238,6 +242,17 @@ mod tests { }); } + #[test] + fn extension_rejects_new_header_if_pallet_is_halted() { + run_test(|| { + // when pallet is halted => tx is rejected + sync_to_header_10(); + PalletOperatingMode::<TestRuntime, ()>::put(BasicOperatingMode::Halted); + + assert!(!validate_block_submit(15)); + }); + } + #[test] fn extension_accepts_new_header() { run_test(|| { diff --git a/bridges/modules/parachains/src/call_ext.rs b/bridges/modules/parachains/src/call_ext.rs index 99640dadc61f4..198ff11be4951 100644 --- a/bridges/modules/parachains/src/call_ext.rs +++ b/bridges/modules/parachains/src/call_ext.rs @@ -17,6 +17,7 @@ use crate::{Config, Pallet, RelayBlockNumber}; use bp_parachains::BestParaHeadHash; use bp_polkadot_core::parachains::{ParaHash, ParaId}; +use bp_runtime::OwnedBridgeModule; use frame_support::{dispatch::CallableCallFor, traits::IsSubType}; use sp_runtime::{ transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, @@ -141,6 +142,10 @@ pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>: None => return Ok(ValidTransaction::default()), }; + if Pallet::<T, I>::ensure_not_halted().is_err() { + return InvalidTransaction::Call.into() + } + if SubmitParachainHeadsHelper::<T, I>::is_obsolete(&update) { return InvalidTransaction::Stale.into() } @@ -160,10 +165,11 @@ where mod tests { use crate::{ mock::{run_test, RuntimeCall, TestRuntime}, - CallSubType, ParaInfo, ParasInfo, RelayBlockNumber, + CallSubType, PalletOperatingMode, ParaInfo, ParasInfo, RelayBlockNumber, }; use bp_parachains::BestParaHeadHash; use bp_polkadot_core::parachains::{ParaHash, ParaHeadsProof, ParaId}; + use bp_runtime::BasicOperatingMode; fn validate_submit_parachain_heads( num: RelayBlockNumber, @@ -221,6 +227,17 @@ mod tests { }); } + #[test] + fn extension_rejects_header_if_pallet_is_halted() { + run_test(|| { + // when pallet is halted => tx is rejected + sync_to_relay_header_10(); + PalletOperatingMode::<TestRuntime, ()>::put(BasicOperatingMode::Halted); + + assert!(!validate_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())])); + }); + } + #[test] fn extension_accepts_new_header() { run_test(|| { From e3c97e486010d443cb20f8bbb29d404527665a1a Mon Sep 17 00:00:00 2001 From: Branislav Kontur <bkontur@gmail.com> Date: Tue, 10 Oct 2023 13:26:22 +0200 Subject: [PATCH 22/54] [xcm] Use `Weight::MAX` for `reserve_asset_deposited`, `receive_teleported_asset` benchmarks (#1726) # Description ## Summary Previously, the `pallet_xcm::do_reserve_transfer_assets` and `pallet_xcm::do_teleport_assets` functions relied on weight estimation for remote chain execution, which was based on guesswork derived from the local chain. This approach led to complications for runtimes that did not provide or support specific [XCM configurations](https://github.com/paritytech/polkadot-sdk/blob/7cbe0c76ef8fd2aabf9f07de0156941ce3ed44b0/polkadot/xcm/xcm-executor/src/config.rs#L43-L47) for `IsReserve` or `IsTeleporter`. Consequently, such runtimes had to resort to implementing hard-coded weights for XCM instructions like `reserve_asset_deposited` or `receive_teleported_asset` to support extrinsics such as `pallet_xcm::reserve_transfer_assets` and `pallet_xcm::teleport_assets`, which depended on remote weight estimation. The issue of remote weight estimation was addressed and resolved by [Pull Request #1645](https://github.com/paritytech/polkadot-sdk/pull/1645), which removed the need for remote weight estimation. ## Solution As a continuation of this improvement, the current PR proposes further cleanup by removing unnecessary hard-coded values and rectifying benchmark results with `Weight::MAX` that previously used `T::BlockWeights::get().max_block` as an override for unsupported XCM instructions like `ReserveAssetDeposited` and `ReceiveTeleportedAsset`. ## Questions - [x] Can we remove now also `Hardcoded till the XCM pallet is fixed` for `deposit_asset`? E.g. for AssetHubKusama [here](https://github.com/paritytech/polkadot-sdk/blob/7cbe0c76ef8fd2aabf9f07de0156941ce3ed44b0/cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/xcm/mod.rs#L129-L134) - [x] Are comments like [this](https://github.com/paritytech/polkadot-sdk/blob/7cbe0c76ef8fd2aabf9f07de0156941ce3ed44b0/polkadot/runtime/kusama/src/weights/xcm/mod.rs#L94) `// Kusama doesn't support ReserveAssetDeposited, so this benchmark has a default weight` still relevant? Shouldnt be removed/changed? ## TODO - [x] `bench bot` regenerate xcm weights for all runtimes - [x] remove hard-coded stuff from system parachain weight files - [ ] when merged, open `polkadot-fellow/runtimes` PR ## References Fixes #1132 Closes #1132 Old polkadot repo [PR](https://github.com/paritytech/polkadot/pull/7546) --------- Co-authored-by: command-bot <> --- .../src/tests/reserve_transfer.rs | 11 +- .../asset-hub-westend/src/tests/send.rs | 4 +- .../asset-hub-westend/src/tests/teleport.rs | 168 +++++++++--------- .../emulated/common/src/impls.rs | 16 ++ .../asset-hub-kusama/src/weights/xcm/mod.rs | 17 +- .../xcm/pallet_xcm_benchmarks_fungible.rs | 78 ++++---- .../asset-hub-polkadot/src/weights/xcm/mod.rs | 17 +- .../xcm/pallet_xcm_benchmarks_fungible.rs | 64 ++++--- .../asset-hub-westend/src/weights/xcm/mod.rs | 17 +- .../xcm/pallet_xcm_benchmarks_fungible.rs | 64 ++++--- .../bridge-hub-kusama/src/weights/xcm/mod.rs | 17 +- .../xcm/pallet_xcm_benchmarks_fungible.rs | 64 ++++--- .../src/weights/xcm/mod.rs | 22 +-- .../xcm/pallet_xcm_benchmarks_fungible.rs | 64 ++++--- .../bridge-hub-rococo/src/weights/xcm/mod.rs | 17 +- .../xcm/pallet_xcm_benchmarks_fungible.rs | 64 ++++--- .../runtime/rococo/src/weights/xcm/mod.rs | 1 - .../xcm/pallet_xcm_benchmarks_fungible.rs | 64 +++---- .../runtime/westend/src/weights/xcm/mod.rs | 1 - .../xcm/pallet_xcm_benchmarks_fungible.rs | 60 +++---- .../src/fungible/benchmarking.rs | 5 +- 21 files changed, 381 insertions(+), 454 deletions(-) diff --git a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/reserve_transfer.rs b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/reserve_transfer.rs index 51fac43be1255..805c8811b2ddf 100644 --- a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/reserve_transfer.rs +++ b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/reserve_transfer.rs @@ -35,11 +35,8 @@ fn relay_origin_assertions(t: RelayToSystemParaTest) { ); } -fn system_para_dest_assertions_incomplete(_t: RelayToSystemParaTest) { - AssetHubWestend::assert_dmp_queue_incomplete( - Some(Weight::from_parts(1_000_000_000, 0)), - Some(Error::UntrustedReserveLocation), - ); +fn system_para_dest_assertions(_t: RelayToSystemParaTest) { + AssetHubWestend::assert_dmp_queue_error(Error::WeightNotComputable); } fn system_para_to_relay_assertions(_t: SystemParaToRelayTest) { @@ -178,7 +175,7 @@ fn limited_reserve_transfer_native_asset_from_relay_to_system_para_fails() { let receiver_balance_before = test.receiver.balance; test.set_assertion::<Westend>(relay_origin_assertions); - test.set_assertion::<AssetHubWestend>(system_para_dest_assertions_incomplete); + test.set_assertion::<AssetHubWestend>(system_para_dest_assertions); test.set_dispatchable::<Westend>(relay_limited_reserve_transfer_assets); test.assert(); @@ -237,7 +234,7 @@ fn reserve_transfer_native_asset_from_relay_to_system_para_fails() { let receiver_balance_before = test.receiver.balance; test.set_assertion::<Westend>(relay_origin_assertions); - test.set_assertion::<AssetHubWestend>(system_para_dest_assertions_incomplete); + test.set_assertion::<AssetHubWestend>(system_para_dest_assertions); test.set_dispatchable::<Westend>(relay_reserve_transfer_assets); test.assert(); diff --git a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/send.rs b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/send.rs index 424d222bef381..d0a71e88c674a 100644 --- a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/send.rs +++ b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/send.rs @@ -119,8 +119,8 @@ fn send_xcm_from_para_to_system_para_paying_fee_with_assets_works() { type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent; AssetHubWestend::assert_xcmp_queue_success(Some(Weight::from_parts( - 2_176_414_000, - 203_593, + 16_290_336_000, + 562_893, ))); assert_expected_events!( diff --git a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/teleport.rs b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/teleport.rs index 8de73a7420c6b..d94fd4b97d9fc 100644 --- a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/teleport.rs +++ b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/teleport.rs @@ -97,7 +97,7 @@ fn para_origin_assertions(t: SystemParaToRelayTest) { fn para_dest_assertions(t: RelayToSystemParaTest) { type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent; - AssetHubWestend::assert_dmp_queue_complete(Some(Weight::from_parts(164_733_000, 0))); + AssetHubWestend::assert_dmp_queue_complete(Some(Weight::from_parts(164_793_000, 3593))); assert_expected_events!( AssetHubWestend, @@ -142,16 +142,15 @@ fn system_para_limited_teleport_assets(t: SystemParaToRelayTest) -> DispatchResu ) } -// TODO: Uncomment when https://github.com/paritytech/polkadot/pull/7424 is merged -// fn system_para_teleport_assets(t: SystemParaToRelayTest) -> DispatchResult { -// <AssetHubWestend as AssetHubWestendPallet>::PolkadotXcm::teleport_assets( -// t.signed_origin, -// bx!(t.args.dest), -// bx!(t.args.beneficiary), -// bx!(t.args.assets), -// t.args.fee_asset_item, -// ) -// } +fn system_para_teleport_assets(t: SystemParaToRelayTest) -> DispatchResult { + <AssetHubWestend as AssetHubWestendPallet>::PolkadotXcm::teleport_assets( + t.signed_origin, + bx!(t.args.dest.into()), + bx!(t.args.beneficiary.into()), + bx!(t.args.assets.into()), + t.args.fee_asset_item, + ) +} /// Limited Teleport of native asset from Relay Chain to the System Parachain should work #[test] @@ -286,78 +285,75 @@ fn teleport_native_assets_from_relay_to_system_para_works() { assert!(receiver_balance_after > receiver_balance_before); } -// TODO: Uncomment when https://github.com/paritytech/polkadot/pull/7424 is merged - -// Right now it is failing in the Relay Chain with a -// `messageQueue.ProcessingFailed` event `error: Unsupported`. -// The reason is the `Weigher` in `pallet_xcm` is not properly calculating the `remote_weight` -// and it cause an `Overweight` error in `AllowTopLevelPaidExecutionFrom` barrier - -// /// Teleport of native asset from System Parachains to the Relay Chain -// /// should work when there is enough balance in Relay Chain's `CheckAccount` -// #[test] -// fn teleport_native_assets_back_from_system_para_to_relay_works() { -// // Dependency - Relay Chain's `CheckAccount` should have enough balance -// teleport_native_assets_from_relay_to_system_para_works(); - -// // Init values for Relay Chain -// let amount_to_send: Balance = ASSET_HUB_WESTEND_ED * 1000; -// let test_args = TestContext { -// sender: AssetHubWestendSender::get(), -// receiver: WestendReceiver::get(), -// args: get_para_dispatch_args(amount_to_send), -// }; - -// let mut test = SystemParaToRelayTest::new(test_args); - -// let sender_balance_before = test.sender.balance; -// let receiver_balance_before = test.receiver.balance; - -// test.set_assertion::<AssetHubWestend>(para_origin_assertions); -// test.set_assertion::<Westend>(relay_dest_assertions); -// test.set_dispatchable::<AssetHubWestend>(system_para_teleport_assets); -// test.assert(); - -// let sender_balance_after = test.sender.balance; -// let receiver_balance_after = test.receiver.balance; - -// // Sender's balance is reduced -// assert_eq!(sender_balance_before - amount_to_send, sender_balance_after); -// // Receiver's balance is increased -// assert!(receiver_balance_after > receiver_balance_before); -// } - -// /// Teleport of native asset from System Parachain to Relay Chain -// /// shouldn't work when there is not enough balance in Relay Chain's `CheckAccount` -// #[test] -// fn teleport_native_assets_from_system_para_to_relay_fails() { -// // Init values for Relay Chain -// let amount_to_send: Balance = ASSET_HUB_WESTEND_ED * 1000; -// let assets = (Parent, amount_to_send).into(); -// -// let test_args = TestContext { -// sender: AssetHubWestendSender::get(), -// receiver: WestendReceiver::get(), -// args: system_para_test_args(amount_to_send), -// assets, -// None -// }; - -// let mut test = SystemParaToRelayTest::new(test_args); - -// let sender_balance_before = test.sender.balance; -// let receiver_balance_before = test.receiver.balance; - -// test.set_assertion::<AssetHubWestend>(para_origin_assertions); -// test.set_assertion::<Westend>(relay_dest_assertions); -// test.set_dispatchable::<AssetHubWestend>(system_para_teleport_assets); -// test.assert(); - -// let sender_balance_after = test.sender.balance; -// let receiver_balance_after = test.receiver.balance; - -// // Sender's balance is reduced -// assert_eq!(sender_balance_before - amount_to_send, sender_balance_after); -// // Receiver's balance does not change -// assert_eq!(receiver_balance_after, receiver_balance_before); -// } +/// Teleport of native asset from System Parachains to the Relay Chain +/// should work when there is enough balance in Relay Chain's `CheckAccount` +#[test] +fn teleport_native_assets_back_from_system_para_to_relay_works() { + // Dependency - Relay Chain's `CheckAccount` should have enough balance + teleport_native_assets_from_relay_to_system_para_works(); + + // Init values for Relay Chain + let amount_to_send: Balance = ASSET_HUB_WESTEND_ED * 1000; + let destination = AssetHubWestend::parent_location(); + let beneficiary_id = WestendReceiver::get(); + let assets = (Parent, amount_to_send).into(); + + let test_args = TestContext { + sender: AssetHubWestendSender::get(), + receiver: WestendReceiver::get(), + args: system_para_test_args(destination, beneficiary_id, amount_to_send, assets, None), + }; + + let mut test = SystemParaToRelayTest::new(test_args); + + let sender_balance_before = test.sender.balance; + let receiver_balance_before = test.receiver.balance; + + test.set_assertion::<AssetHubWestend>(para_origin_assertions); + test.set_assertion::<Westend>(relay_dest_assertions); + test.set_dispatchable::<AssetHubWestend>(system_para_teleport_assets); + test.assert(); + + let sender_balance_after = test.sender.balance; + let receiver_balance_after = test.receiver.balance; + + // Sender's balance is reduced + assert_eq!(sender_balance_before - amount_to_send, sender_balance_after); + // Receiver's balance is increased + assert!(receiver_balance_after > receiver_balance_before); +} + +/// Teleport of native asset from System Parachain to Relay Chain +/// shouldn't work when there is not enough balance in Relay Chain's `CheckAccount` +#[test] +fn teleport_native_assets_from_system_para_to_relay_fails() { + // Init values for Relay Chain + let amount_to_send: Balance = ASSET_HUB_WESTEND_ED * 1000; + let destination = AssetHubWestend::parent_location(); + let beneficiary_id = WestendReceiver::get(); + let assets = (Parent, amount_to_send).into(); + + let test_args = TestContext { + sender: AssetHubWestendSender::get(), + receiver: WestendReceiver::get(), + args: system_para_test_args(destination, beneficiary_id, amount_to_send, assets, None), + }; + + let mut test = SystemParaToRelayTest::new(test_args); + + let sender_balance_before = test.sender.balance; + let receiver_balance_before = test.receiver.balance; + + test.set_assertion::<AssetHubWestend>(para_origin_assertions); + test.set_assertion::<Westend>(relay_dest_assertions_fail); + test.set_dispatchable::<AssetHubWestend>(system_para_teleport_assets); + test.assert(); + + let sender_balance_after = test.sender.balance; + let receiver_balance_after = test.receiver.balance; + + // Sender's balance is reduced + assert_eq!(sender_balance_before - amount_to_send, sender_balance_after); + // Receiver's balance does not change + assert_eq!(receiver_balance_after, receiver_balance_before); +} diff --git a/cumulus/parachains/integration-tests/emulated/common/src/impls.rs b/cumulus/parachains/integration-tests/emulated/common/src/impls.rs index 024ae65c51ee4..5e11922d859c6 100644 --- a/cumulus/parachains/integration-tests/emulated/common/src/impls.rs +++ b/cumulus/parachains/integration-tests/emulated/common/src/impls.rs @@ -503,6 +503,22 @@ macro_rules! impl_assert_events_helpers_for_parachain { ); } + /// Asserts a XCM from Relay Chain is executed with error + pub fn assert_dmp_queue_error( + expected_error: $crate::impls::Error, + ) { + $crate::impls::assert_expected_events!( + Self, + vec![ + [<$chain RuntimeEvent>]::DmpQueue($crate::impls::cumulus_pallet_dmp_queue::Event::ExecutedDownward { + outcome: $crate::impls::Outcome::Error(error), .. + }) => { + error: *error == expected_error, + }, + ] + ); + } + /// Asserts a XCM from another Parachain is completely executed pub fn assert_xcmp_queue_success(expected_weight: Option<$crate::impls::Weight>) { $crate::impls::assert_expected_events!( diff --git a/cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/xcm/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/xcm/mod.rs index 9aff4902d15ba..ce6e92065156e 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/xcm/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/xcm/mod.rs @@ -61,16 +61,8 @@ impl<Call> XcmWeightInfo<Call> for AssetHubKusamaXcmWeight<Call> { fn withdraw_asset(assets: &MultiAssets) -> Weight { assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::withdraw_asset()) } - // Currently there is no trusted reserve (`IsReserve = ()`), - // but we need this hack for `pallet_xcm::reserve_transfer_assets` - // (TODO) fix https://github.com/paritytech/polkadot/pull/7424 - // (TODO) fix https://github.com/paritytech/polkadot/pull/7546 - fn reserve_asset_deposited(_assets: &MultiAssets) -> Weight { - // TODO: if we change `IsReserve = ...` then use this line... - // TODO: or if remote weight estimation is fixed, then remove - // TODO: hardcoded - fix https://github.com/paritytech/cumulus/issues/1974 - let hardcoded_weight = Weight::from_parts(1_000_000_000_u64, 0); - hardcoded_weight.min(XcmFungibleWeight::<Runtime>::reserve_asset_deposited()) + fn reserve_asset_deposited(assets: &MultiAssets) -> Weight { + assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::reserve_asset_deposited()) } fn receive_teleported_asset(assets: &MultiAssets) -> Weight { assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::receive_teleported_asset()) @@ -127,10 +119,7 @@ impl<Call> XcmWeightInfo<Call> for AssetHubKusamaXcmWeight<Call> { } fn deposit_asset(assets: &MultiAssetFilter, _dest: &MultiLocation) -> Weight { - // Hardcoded till the XCM pallet is fixed - let hardcoded_weight = Weight::from_parts(1_000_000_000_u64, 0); - let weight = assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::deposit_asset()); - hardcoded_weight.min(weight) + assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::deposit_asset()) } fn deposit_reserve_asset( assets: &MultiAssetFilter, diff --git a/cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs b/cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs index 6e663039b0c2b..9b8611fd6637a 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs @@ -17,28 +17,26 @@ //! Autogenerated weights for `pallet_xcm_benchmarks::fungible` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-07-31, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-09-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runner-ynta1nyy-project-238-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("asset-hub-kusama-dev"), DB CACHE: 1024 +//! HOSTNAME: `runner-nbnwcyh-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: Compiled, CHAIN: Some("asset-hub-kusama-dev"), DB CACHE: 1024 // Executed Command: -// ./target/production/polkadot-parachain +// target/production/polkadot-parachain // benchmark // pallet -// --template=./templates/xcm-bench-template.hbs -// --chain=asset-hub-kusama-dev -// --wasm-execution=compiled -// --pallet=pallet_xcm_benchmarks::fungible -// --no-storage-info -// --no-median-slopes -// --no-min-squares -// --extrinsic=* // --steps=50 // --repeat=20 -// --json -// --header=./file_header.txt -// --output=./parachains/runtimes/assets/asset-hub-kusama/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json +// --pallet=pallet_xcm_benchmarks::fungible +// --chain=asset-hub-kusama-dev +// --header=./cumulus/file_header.txt +// --template=./cumulus/templates/xcm-bench-template.hbs +// --output=./cumulus/parachains/runtimes/assets/asset-hub-kusama/src/weights/xcm/ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -56,8 +54,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `101` // Estimated: `3593` - // Minimum execution time: 26_104_000 picoseconds. - Weight::from_parts(26_722_000, 3593) + // Minimum execution time: 25_602_000 picoseconds. + Weight::from_parts(26_312_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -67,8 +65,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `101` // Estimated: `6196` - // Minimum execution time: 52_259_000 picoseconds. - Weight::from_parts(53_854_000, 6196) + // Minimum execution time: 51_173_000 picoseconds. + Weight::from_parts(52_221_000, 6196) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -88,10 +86,10 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) pub fn transfer_reserve_asset() -> Weight { // Proof Size summary in bytes: - // Measured: `210` + // Measured: `246` // Estimated: `6196` - // Minimum execution time: 77_248_000 picoseconds. - Weight::from_parts(80_354_000, 6196) + // Minimum execution time: 74_651_000 picoseconds. + Weight::from_parts(76_500_000, 6196) .saturating_add(T::DbWeight::get().reads(8)) .saturating_add(T::DbWeight::get().writes(4)) } @@ -101,8 +99,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 500_000_000_000 picoseconds. - Weight::from_parts(500_000_000_000, 0) + // Minimum execution time: 18_446_744_073_709_551_000 picoseconds. + Weight::from_parts(18_446_744_073_709_551_000, 0) } // Storage: `ParachainInfo::ParachainId` (r:1 w:0) // Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -118,10 +116,10 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) pub fn initiate_reserve_withdraw() -> Weight { // Proof Size summary in bytes: - // Measured: `109` - // Estimated: `3574` - // Minimum execution time: 482_070_000 picoseconds. - Weight::from_parts(490_269_000, 3574) + // Measured: `145` + // Estimated: `3610` + // Minimum execution time: 458_666_000 picoseconds. + Weight::from_parts(470_470_000, 3610) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -129,8 +127,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 3_970_000 picoseconds. - Weight::from_parts(4_056_000, 0) + // Minimum execution time: 3_701_000 picoseconds. + Weight::from_parts(3_887_000, 0) } // Storage: `System::Account` (r:1 w:1) // Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) @@ -138,8 +136,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `3593` - // Minimum execution time: 26_324_000 picoseconds. - Weight::from_parts(26_985_000, 3593) + // Minimum execution time: 25_709_000 picoseconds. + Weight::from_parts(26_320_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -159,10 +157,10 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) pub fn deposit_reserve_asset() -> Weight { // Proof Size summary in bytes: - // Measured: `109` - // Estimated: `3593` - // Minimum execution time: 52_814_000 picoseconds. - Weight::from_parts(54_666_000, 3593) + // Measured: `145` + // Estimated: `3610` + // Minimum execution time: 51_663_000 picoseconds. + Weight::from_parts(52_538_000, 3610) .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -180,10 +178,10 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof: `ParachainSystem::PendingUpwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) pub fn initiate_teleport() -> Weight { // Proof Size summary in bytes: - // Measured: `109` - // Estimated: `3574` - // Minimum execution time: 33_044_000 picoseconds. - Weight::from_parts(33_849_000, 3574) + // Measured: `145` + // Estimated: `3610` + // Minimum execution time: 31_972_000 picoseconds. + Weight::from_parts(32_834_000, 3610) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(2)) } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-polkadot/src/weights/xcm/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-polkadot/src/weights/xcm/mod.rs index 55fed809e2b75..eb140c4bf3238 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-polkadot/src/weights/xcm/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-polkadot/src/weights/xcm/mod.rs @@ -61,16 +61,8 @@ impl<Call> XcmWeightInfo<Call> for AssetHubPolkadotXcmWeight<Call> { fn withdraw_asset(assets: &MultiAssets) -> Weight { assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::withdraw_asset()) } - // Currently there is no trusted reserve (`IsReserve = ()`), - // but we need this hack for `pallet_xcm::reserve_transfer_assets` - // (TODO) fix https://github.com/paritytech/polkadot/pull/7424 - // (TODO) fix https://github.com/paritytech/polkadot/pull/7546 - fn reserve_asset_deposited(_assets: &MultiAssets) -> Weight { - // TODO: if we change `IsReserve = ...` then use this line... - // TODO: or if remote weight estimation is fixed, then remove - // TODO: hardcoded - fix https://github.com/paritytech/cumulus/issues/1974 - let hardcoded_weight = Weight::from_parts(1_000_000_000_u64, 0); - hardcoded_weight.min(XcmFungibleWeight::<Runtime>::reserve_asset_deposited()) + fn reserve_asset_deposited(assets: &MultiAssets) -> Weight { + assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::reserve_asset_deposited()) } fn receive_teleported_asset(assets: &MultiAssets) -> Weight { assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::receive_teleported_asset()) @@ -127,10 +119,7 @@ impl<Call> XcmWeightInfo<Call> for AssetHubPolkadotXcmWeight<Call> { } fn deposit_asset(assets: &MultiAssetFilter, _dest: &MultiLocation) -> Weight { - // Hardcoded till the XCM pallet is fixed - let hardcoded_weight = Weight::from_parts(1_000_000_000_u64, 0); - let weight = assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::deposit_asset()); - hardcoded_weight.min(weight) + assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::deposit_asset()) } fn deposit_reserve_asset( assets: &MultiAssetFilter, diff --git a/cumulus/parachains/runtimes/assets/asset-hub-polkadot/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs b/cumulus/parachains/runtimes/assets/asset-hub-polkadot/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs index 4f64ea3fa1bb3..96d86ec423f20 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-polkadot/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-polkadot/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs @@ -17,28 +17,26 @@ //! Autogenerated weights for `pallet_xcm_benchmarks::fungible` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-07-31, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-09-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runner-ynta1nyy-project-238-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("asset-hub-polkadot-dev"), DB CACHE: 1024 +//! HOSTNAME: `runner-nbnwcyh-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: Compiled, CHAIN: Some("asset-hub-polkadot-dev"), DB CACHE: 1024 // Executed Command: -// ./target/production/polkadot-parachain +// target/production/polkadot-parachain // benchmark // pallet -// --template=./templates/xcm-bench-template.hbs -// --chain=asset-hub-polkadot-dev -// --wasm-execution=compiled -// --pallet=pallet_xcm_benchmarks::fungible -// --no-storage-info -// --no-median-slopes -// --no-min-squares -// --extrinsic=* // --steps=50 // --repeat=20 -// --json -// --header=./file_header.txt -// --output=./parachains/runtimes/assets/asset-hub-polkadot/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json +// --pallet=pallet_xcm_benchmarks::fungible +// --chain=asset-hub-polkadot-dev +// --header=./cumulus/file_header.txt +// --template=./cumulus/templates/xcm-bench-template.hbs +// --output=./cumulus/parachains/runtimes/assets/asset-hub-polkadot/src/weights/xcm/ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -56,8 +54,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `101` // Estimated: `3593` - // Minimum execution time: 26_090_000 picoseconds. - Weight::from_parts(27_006_000, 3593) + // Minimum execution time: 25_903_000 picoseconds. + Weight::from_parts(26_768_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -67,8 +65,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `101` // Estimated: `6196` - // Minimum execution time: 50_699_000 picoseconds. - Weight::from_parts(51_888_000, 6196) + // Minimum execution time: 51_042_000 picoseconds. + Weight::from_parts(51_939_000, 6196) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -90,8 +88,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `176` // Estimated: `6196` - // Minimum execution time: 72_130_000 picoseconds. - Weight::from_parts(73_994_000, 6196) + // Minimum execution time: 74_626_000 picoseconds. + Weight::from_parts(75_963_000, 6196) .saturating_add(T::DbWeight::get().reads(8)) .saturating_add(T::DbWeight::get().writes(4)) } @@ -101,8 +99,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 500_000_000_000 picoseconds. - Weight::from_parts(500_000_000_000, 0) + // Minimum execution time: 18_446_744_073_709_551_000 picoseconds. + Weight::from_parts(18_446_744_073_709_551_000, 0) } // Storage: `ParachainInfo::ParachainId` (r:1 w:0) // Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -120,8 +118,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `75` // Estimated: `3540` - // Minimum execution time: 477_183_000 picoseconds. - Weight::from_parts(488_156_000, 3540) + // Minimum execution time: 480_030_000 picoseconds. + Weight::from_parts(486_039_000, 3540) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -129,8 +127,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 3_966_000 picoseconds. - Weight::from_parts(4_129_000, 0) + // Minimum execution time: 3_936_000 picoseconds. + Weight::from_parts(4_033_000, 0) } // Storage: `System::Account` (r:1 w:1) // Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) @@ -138,8 +136,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `3593` - // Minimum execution time: 26_047_000 picoseconds. - Weight::from_parts(26_982_000, 3593) + // Minimum execution time: 26_274_000 picoseconds. + Weight::from_parts(26_609_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -161,8 +159,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `75` // Estimated: `3593` - // Minimum execution time: 51_076_000 picoseconds. - Weight::from_parts(51_826_000, 3593) + // Minimum execution time: 52_888_000 picoseconds. + Weight::from_parts(53_835_000, 3593) .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -182,8 +180,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `75` // Estimated: `3540` - // Minimum execution time: 30_606_000 picoseconds. - Weight::from_parts(31_168_000, 3540) + // Minimum execution time: 33_395_000 picoseconds. + Weight::from_parts(33_827_000, 3540) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(2)) } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/mod.rs index bb850ac72c07d..3e47cf077a292 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/mod.rs @@ -61,16 +61,8 @@ impl<Call> XcmWeightInfo<Call> for AssetHubWestendXcmWeight<Call> { fn withdraw_asset(assets: &MultiAssets) -> Weight { assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::withdraw_asset()) } - // Currently there is no trusted reserve (`IsReserve = ()`), - // but we need this hack for `pallet_xcm::reserve_transfer_assets` - // (TODO) fix https://github.com/paritytech/polkadot/pull/7424 - // (TODO) fix https://github.com/paritytech/polkadot/pull/7546 - fn reserve_asset_deposited(_assets: &MultiAssets) -> Weight { - // TODO: if we change `IsReserve = ...` then use this line... - // TODO: or if remote weight estimation is fixed, then remove - // TODO: hardcoded - fix https://github.com/paritytech/cumulus/issues/1974 - let hardcoded_weight = Weight::from_parts(1_000_000_000_u64, 0); - hardcoded_weight.min(XcmFungibleWeight::<Runtime>::reserve_asset_deposited()) + fn reserve_asset_deposited(assets: &MultiAssets) -> Weight { + assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::reserve_asset_deposited()) } fn receive_teleported_asset(assets: &MultiAssets) -> Weight { assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::receive_teleported_asset()) @@ -127,10 +119,7 @@ impl<Call> XcmWeightInfo<Call> for AssetHubWestendXcmWeight<Call> { } fn deposit_asset(assets: &MultiAssetFilter, _dest: &MultiLocation) -> Weight { - // Hardcoded till the XCM pallet is fixed - let hardcoded_weight = Weight::from_parts(1_000_000_000_u64, 0); - let weight = assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::deposit_asset()); - hardcoded_weight.min(weight) + assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::deposit_asset()) } fn deposit_reserve_asset( assets: &MultiAssetFilter, diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs index d6763d2fc66f4..f482064e84e9c 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs @@ -17,28 +17,26 @@ //! Autogenerated weights for `pallet_xcm_benchmarks::fungible` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-07-31, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-09-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runner-ynta1nyy-project-238-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("asset-hub-westend-dev"), DB CACHE: 1024 +//! HOSTNAME: `runner-nbnwcyh-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: Compiled, CHAIN: Some("asset-hub-westend-dev"), DB CACHE: 1024 // Executed Command: -// ./target/production/polkadot-parachain +// target/production/polkadot-parachain // benchmark // pallet -// --template=./templates/xcm-bench-template.hbs -// --chain=asset-hub-westend-dev -// --wasm-execution=compiled -// --pallet=pallet_xcm_benchmarks::fungible -// --no-storage-info -// --no-median-slopes -// --no-min-squares -// --extrinsic=* // --steps=50 // --repeat=20 -// --json -// --header=./file_header.txt -// --output=./parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json +// --pallet=pallet_xcm_benchmarks::fungible +// --chain=asset-hub-westend-dev +// --header=./cumulus/file_header.txt +// --template=./cumulus/templates/xcm-bench-template.hbs +// --output=./cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -56,8 +54,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `101` // Estimated: `3593` - // Minimum execution time: 25_411_000 picoseconds. - Weight::from_parts(25_663_000, 3593) + // Minimum execution time: 25_407_000 picoseconds. + Weight::from_parts(25_949_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -67,8 +65,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `101` // Estimated: `6196` - // Minimum execution time: 49_478_000 picoseconds. - Weight::from_parts(50_417_000, 6196) + // Minimum execution time: 51_335_000 picoseconds. + Weight::from_parts(52_090_000, 6196) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -90,8 +88,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `246` // Estimated: `6196` - // Minimum execution time: 72_958_000 picoseconds. - Weight::from_parts(74_503_000, 6196) + // Minimum execution time: 74_312_000 picoseconds. + Weight::from_parts(76_725_000, 6196) .saturating_add(T::DbWeight::get().reads(8)) .saturating_add(T::DbWeight::get().writes(4)) } @@ -101,8 +99,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 500_000_000_000 picoseconds. - Weight::from_parts(500_000_000_000, 0) + // Minimum execution time: 18_446_744_073_709_551_000 picoseconds. + Weight::from_parts(18_446_744_073_709_551_000, 0) } // Storage: `ParachainInfo::ParachainId` (r:1 w:0) // Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -120,8 +118,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `145` // Estimated: `3610` - // Minimum execution time: 456_993_000 picoseconds. - Weight::from_parts(469_393_000, 3610) + // Minimum execution time: 446_848_000 picoseconds. + Weight::from_parts(466_251_000, 3610) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -129,8 +127,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 3_580_000 picoseconds. - Weight::from_parts(3_717_000, 0) + // Minimum execution time: 3_602_000 picoseconds. + Weight::from_parts(3_844_000, 0) } // Storage: `System::Account` (r:1 w:1) // Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) @@ -138,8 +136,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `3593` - // Minimum execution time: 25_087_000 picoseconds. - Weight::from_parts(25_788_000, 3593) + // Minimum execution time: 25_480_000 picoseconds. + Weight::from_parts(26_142_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -161,8 +159,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `145` // Estimated: `3610` - // Minimum execution time: 50_824_000 picoseconds. - Weight::from_parts(52_309_000, 3610) + // Minimum execution time: 51_540_000 picoseconds. + Weight::from_parts(53_744_000, 3610) .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -182,8 +180,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `145` // Estimated: `3610` - // Minimum execution time: 31_854_000 picoseconds. - Weight::from_parts(32_553_000, 3610) + // Minimum execution time: 32_279_000 picoseconds. + Weight::from_parts(33_176_000, 3610) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(2)) } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/xcm/mod.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/xcm/mod.rs index 0e740922f339d..ded5dc6702e60 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/xcm/mod.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/xcm/mod.rs @@ -61,16 +61,8 @@ impl<Call> XcmWeightInfo<Call> for BridgeHubKusamaXcmWeight<Call> { fn withdraw_asset(assets: &MultiAssets) -> Weight { assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::withdraw_asset()) } - // Currently there is no trusted reserve (`IsReserve = ()`), - // but we need this hack for `pallet_xcm::reserve_transfer_assets` - // (TODO) fix https://github.com/paritytech/polkadot/pull/7424 - // (TODO) fix https://github.com/paritytech/polkadot/pull/7546 - fn reserve_asset_deposited(_assets: &MultiAssets) -> Weight { - // TODO: if we change `IsReserve = ...` then use this line... - // TODO: or if remote weight estimation is fixed, then remove - // TODO: hardcoded - fix https://github.com/paritytech/cumulus/issues/1974 - let hardcoded_weight = Weight::from_parts(1_000_000_000_u64, 0); - hardcoded_weight.min(XcmFungibleWeight::<Runtime>::reserve_asset_deposited()) + fn reserve_asset_deposited(assets: &MultiAssets) -> Weight { + assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::reserve_asset_deposited()) } fn receive_teleported_asset(assets: &MultiAssets) -> Weight { assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::receive_teleported_asset()) @@ -127,10 +119,7 @@ impl<Call> XcmWeightInfo<Call> for BridgeHubKusamaXcmWeight<Call> { } fn deposit_asset(assets: &MultiAssetFilter, _dest: &MultiLocation) -> Weight { - // Hardcoded till the XCM pallet is fixed - let hardcoded_weight = Weight::from_parts(1_000_000_000_u64, 0); - let weight = assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::deposit_asset()); - hardcoded_weight.min(weight) + assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::deposit_asset()) } fn deposit_reserve_asset( assets: &MultiAssetFilter, diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs index 6c8c7ab66bbdb..17ee5cb6a8dca 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs @@ -17,28 +17,26 @@ //! Autogenerated weights for `pallet_xcm_benchmarks::fungible` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-07-31, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-09-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runner-ynta1nyy-project-238-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("bridge-hub-kusama-dev"), DB CACHE: 1024 +//! HOSTNAME: `runner-nbnwcyh-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: Compiled, CHAIN: Some("bridge-hub-kusama-dev"), DB CACHE: 1024 // Executed Command: -// ./target/production/polkadot-parachain +// target/production/polkadot-parachain // benchmark // pallet -// --template=./templates/xcm-bench-template.hbs -// --chain=bridge-hub-kusama-dev -// --wasm-execution=compiled -// --pallet=pallet_xcm_benchmarks::fungible -// --no-storage-info -// --no-median-slopes -// --no-min-squares -// --extrinsic=* // --steps=50 // --repeat=20 -// --json -// --header=./file_header.txt -// --output=./parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json +// --pallet=pallet_xcm_benchmarks::fungible +// --chain=bridge-hub-kusama-dev +// --header=./cumulus/file_header.txt +// --template=./cumulus/templates/xcm-bench-template.hbs +// --output=./cumulus/parachains/runtimes/bridge-hubs/bridge-hub-kusama/src/weights/xcm/ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -56,8 +54,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `101` // Estimated: `3593` - // Minimum execution time: 24_064_000 picoseconds. - Weight::from_parts(24_751_000, 3593) + // Minimum execution time: 25_447_000 picoseconds. + Weight::from_parts(25_810_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -67,8 +65,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `153` // Estimated: `6196` - // Minimum execution time: 51_097_000 picoseconds. - Weight::from_parts(51_960_000, 6196) + // Minimum execution time: 53_908_000 picoseconds. + Weight::from_parts(54_568_000, 6196) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -90,8 +88,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `223` // Estimated: `6196` - // Minimum execution time: 75_319_000 picoseconds. - Weight::from_parts(77_356_000, 6196) + // Minimum execution time: 79_923_000 picoseconds. + Weight::from_parts(80_790_000, 6196) .saturating_add(T::DbWeight::get().reads(8)) .saturating_add(T::DbWeight::get().writes(4)) } @@ -101,8 +99,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 500_000_000_000 picoseconds. - Weight::from_parts(500_000_000_000, 0) + // Minimum execution time: 18_446_744_073_709_551_000 picoseconds. + Weight::from_parts(18_446_744_073_709_551_000, 0) } // Storage: `ParachainInfo::ParachainId` (r:1 w:0) // Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -120,8 +118,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `70` // Estimated: `3535` - // Minimum execution time: 29_392_000 picoseconds. - Weight::from_parts(29_943_000, 3535) + // Minimum execution time: 31_923_000 picoseconds. + Weight::from_parts(32_499_000, 3535) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -129,8 +127,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 3_637_000 picoseconds. - Weight::from_parts(3_720_000, 0) + // Minimum execution time: 3_903_000 picoseconds. + Weight::from_parts(4_065_000, 0) } // Storage: `System::Account` (r:1 w:1) // Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) @@ -138,8 +136,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `52` // Estimated: `3593` - // Minimum execution time: 25_045_000 picoseconds. - Weight::from_parts(25_546_000, 3593) + // Minimum execution time: 26_987_000 picoseconds. + Weight::from_parts(27_486_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -161,8 +159,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `122` // Estimated: `3593` - // Minimum execution time: 51_450_000 picoseconds. - Weight::from_parts(52_354_000, 3593) + // Minimum execution time: 56_012_000 picoseconds. + Weight::from_parts(58_067_000, 3593) .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -182,8 +180,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `70` // Estimated: `3535` - // Minimum execution time: 29_711_000 picoseconds. - Weight::from_parts(30_759_000, 3535) + // Minimum execution time: 32_350_000 picoseconds. + Weight::from_parts(33_403_000, 3535) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(2)) } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/xcm/mod.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/xcm/mod.rs index 4f8c2dec7a8c8..7e9f21842725e 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/xcm/mod.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/xcm/mod.rs @@ -61,16 +61,8 @@ impl<Call> XcmWeightInfo<Call> for BridgeHubPolkadotXcmWeight<Call> { fn withdraw_asset(assets: &MultiAssets) -> Weight { assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::withdraw_asset()) } - // Currently there is no trusted reserve (`IsReserve = ()`), - // but we need this hack for `pallet_xcm::reserve_transfer_assets` - // (TODO) fix https://github.com/paritytech/polkadot/pull/7424 - // (TODO) fix https://github.com/paritytech/polkadot/pull/7546 - fn reserve_asset_deposited(_assets: &MultiAssets) -> Weight { - // TODO: if we change `IsReserve = ...` then use this line... - // TODO: or if remote weight estimation is fixed, then remove - // TODO: hardcoded - fix https://github.com/paritytech/cumulus/issues/1974 - let hardcoded_weight = Weight::from_parts(1_000_000_000_u64, 0); - hardcoded_weight.min(XcmFungibleWeight::<Runtime>::reserve_asset_deposited()) + fn reserve_asset_deposited(assets: &MultiAssets) -> Weight { + assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::reserve_asset_deposited()) } fn receive_teleported_asset(assets: &MultiAssets) -> Weight { assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::receive_teleported_asset()) @@ -127,10 +119,7 @@ impl<Call> XcmWeightInfo<Call> for BridgeHubPolkadotXcmWeight<Call> { } fn deposit_asset(assets: &MultiAssetFilter, _dest: &MultiLocation) -> Weight { - // Hardcoded till the XCM pallet is fixed - let hardcoded_weight = Weight::from_parts(1_000_000_000_u64, 0); - let weight = assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::deposit_asset()); - hardcoded_weight.min(weight) + assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::deposit_asset()) } fn deposit_reserve_asset( assets: &MultiAssetFilter, @@ -154,10 +143,7 @@ impl<Call> XcmWeightInfo<Call> for BridgeHubPolkadotXcmWeight<Call> { _dest: &MultiLocation, _xcm: &Xcm<()>, ) -> Weight { - // Hardcoded till the XCM pallet is fixed - let hardcoded_weight = Weight::from_parts(200_000_000_u64, 0); - let weight = assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::initiate_teleport()); - hardcoded_weight.min(weight) + assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::initiate_teleport()) } fn report_holding(_response_info: &QueryResponseInfo, _assets: &MultiAssetFilter) -> Weight { XcmGeneric::<Runtime>::report_holding() diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs index 7c525dca051d2..f45f393636528 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs @@ -17,28 +17,26 @@ //! Autogenerated weights for `pallet_xcm_benchmarks::fungible` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-07-31, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-09-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runner-ynta1nyy-project-238-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("bridge-hub-polkadot-dev"), DB CACHE: 1024 +//! HOSTNAME: `runner-nbnwcyh-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: Compiled, CHAIN: Some("bridge-hub-polkadot-dev"), DB CACHE: 1024 // Executed Command: -// ./target/production/polkadot-parachain +// target/production/polkadot-parachain // benchmark // pallet -// --template=./templates/xcm-bench-template.hbs -// --chain=bridge-hub-polkadot-dev -// --wasm-execution=compiled -// --pallet=pallet_xcm_benchmarks::fungible -// --no-storage-info -// --no-median-slopes -// --no-min-squares -// --extrinsic=* // --steps=50 // --repeat=20 -// --json -// --header=./file_header.txt -// --output=./parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json +// --pallet=pallet_xcm_benchmarks::fungible +// --chain=bridge-hub-polkadot-dev +// --header=./cumulus/file_header.txt +// --template=./cumulus/templates/xcm-bench-template.hbs +// --output=./cumulus/parachains/runtimes/bridge-hubs/bridge-hub-polkadot/src/weights/xcm/ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -56,8 +54,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `101` // Estimated: `3593` - // Minimum execution time: 23_862_000 picoseconds. - Weight::from_parts(24_603_000, 3593) + // Minimum execution time: 24_237_000 picoseconds. + Weight::from_parts(24_697_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -67,8 +65,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `153` // Estimated: `6196` - // Minimum execution time: 51_101_000 picoseconds. - Weight::from_parts(51_976_000, 6196) + // Minimum execution time: 52_269_000 picoseconds. + Weight::from_parts(53_848_000, 6196) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -90,8 +88,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `223` // Estimated: `6196` - // Minimum execution time: 72_983_000 picoseconds. - Weight::from_parts(74_099_000, 6196) + // Minimum execution time: 77_611_000 picoseconds. + Weight::from_parts(82_634_000, 6196) .saturating_add(T::DbWeight::get().reads(8)) .saturating_add(T::DbWeight::get().writes(4)) } @@ -101,8 +99,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 500_000_000_000 picoseconds. - Weight::from_parts(500_000_000_000, 0) + // Minimum execution time: 18_446_744_073_709_551_000 picoseconds. + Weight::from_parts(18_446_744_073_709_551_000, 0) } // Storage: `ParachainInfo::ParachainId` (r:1 w:0) // Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -120,8 +118,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `70` // Estimated: `3535` - // Minimum execution time: 27_131_000 picoseconds. - Weight::from_parts(28_062_000, 3535) + // Minimum execution time: 29_506_000 picoseconds. + Weight::from_parts(30_269_000, 3535) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -129,8 +127,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 3_564_000 picoseconds. - Weight::from_parts(3_738_000, 0) + // Minimum execution time: 3_541_000 picoseconds. + Weight::from_parts(3_629_000, 0) } // Storage: `System::Account` (r:1 w:1) // Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) @@ -138,8 +136,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `52` // Estimated: `3593` - // Minimum execution time: 24_453_000 picoseconds. - Weight::from_parts(25_216_000, 3593) + // Minimum execution time: 25_651_000 picoseconds. + Weight::from_parts(26_078_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -161,8 +159,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `122` // Estimated: `3593` - // Minimum execution time: 48_913_000 picoseconds. - Weight::from_parts(50_202_000, 3593) + // Minimum execution time: 52_050_000 picoseconds. + Weight::from_parts(53_293_000, 3593) .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -182,8 +180,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `70` // Estimated: `3535` - // Minimum execution time: 27_592_000 picoseconds. - Weight::from_parts(28_099_000, 3535) + // Minimum execution time: 30_009_000 picoseconds. + Weight::from_parts(30_540_000, 3535) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(2)) } diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/xcm/mod.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/xcm/mod.rs index 40a2036fb49a9..78a0eed91740d 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/xcm/mod.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/xcm/mod.rs @@ -62,16 +62,8 @@ impl<Call> XcmWeightInfo<Call> for BridgeHubRococoXcmWeight<Call> { fn withdraw_asset(assets: &MultiAssets) -> Weight { assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::withdraw_asset()) } - // Currently there is no trusted reserve (`IsReserve = ()`), - // but we need this hack for `pallet_xcm::reserve_transfer_assets` - // (TODO) fix https://github.com/paritytech/polkadot/pull/7424 - // (TODO) fix https://github.com/paritytech/polkadot/pull/7546 - fn reserve_asset_deposited(_assets: &MultiAssets) -> Weight { - // TODO: if we change `IsReserve = ...` then use this line... - // TODO: or if remote weight estimation is fixed, then remove - // TODO: hardcoded - fix https://github.com/paritytech/cumulus/issues/1974 - let hardcoded_weight = Weight::from_parts(1_000_000_000_u64, 0); - hardcoded_weight.min(XcmFungibleWeight::<Runtime>::reserve_asset_deposited()) + fn reserve_asset_deposited(assets: &MultiAssets) -> Weight { + assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::reserve_asset_deposited()) } fn receive_teleported_asset(assets: &MultiAssets) -> Weight { assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::receive_teleported_asset()) @@ -128,10 +120,7 @@ impl<Call> XcmWeightInfo<Call> for BridgeHubRococoXcmWeight<Call> { } fn deposit_asset(assets: &MultiAssetFilter, _dest: &MultiLocation) -> Weight { - // Hardcoded till the XCM pallet is fixed - let hardcoded_weight = Weight::from_parts(1_000_000_000_u64, 0); - let weight = assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::deposit_asset()); - hardcoded_weight.min(weight) + assets.weigh_multi_assets(XcmFungibleWeight::<Runtime>::deposit_asset()) } fn deposit_reserve_asset( assets: &MultiAssetFilter, diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs index 8f9fbc912454b..cd1a673cb5397 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs @@ -17,28 +17,26 @@ //! Autogenerated weights for `pallet_xcm_benchmarks::fungible` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-07-31, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-09-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runner-ynta1nyy-project-238-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("bridge-hub-rococo-dev"), DB CACHE: 1024 +//! HOSTNAME: `runner-nbnwcyh-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: Compiled, CHAIN: Some("bridge-hub-rococo-dev"), DB CACHE: 1024 // Executed Command: -// ./target/production/polkadot-parachain +// target/production/polkadot-parachain // benchmark // pallet -// --template=./templates/xcm-bench-template.hbs -// --chain=bridge-hub-rococo-dev -// --wasm-execution=compiled -// --pallet=pallet_xcm_benchmarks::fungible -// --no-storage-info -// --no-median-slopes -// --no-min-squares -// --extrinsic=* // --steps=50 // --repeat=20 -// --json -// --header=./file_header.txt -// --output=./parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json +// --pallet=pallet_xcm_benchmarks::fungible +// --chain=bridge-hub-rococo-dev +// --header=./cumulus/file_header.txt +// --template=./cumulus/templates/xcm-bench-template.hbs +// --output=./cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/xcm/ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -56,8 +54,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `101` // Estimated: `3593` - // Minimum execution time: 24_521_000 picoseconds. - Weight::from_parts(25_005_000, 3593) + // Minimum execution time: 23_601_000 picoseconds. + Weight::from_parts(24_226_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -67,8 +65,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `153` // Estimated: `6196` - // Minimum execution time: 52_274_000 picoseconds. - Weight::from_parts(53_374_000, 6196) + // Minimum execution time: 51_043_000 picoseconds. + Weight::from_parts(52_326_000, 6196) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -90,8 +88,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `260` // Estimated: `6196` - // Minimum execution time: 77_625_000 picoseconds. - Weight::from_parts(78_530_000, 6196) + // Minimum execution time: 75_639_000 picoseconds. + Weight::from_parts(76_736_000, 6196) .saturating_add(T::DbWeight::get().reads(8)) .saturating_add(T::DbWeight::get().writes(4)) } @@ -101,8 +99,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 500_000_000_000 picoseconds. - Weight::from_parts(500_000_000_000, 0) + // Minimum execution time: 18_446_744_073_709_551_000 picoseconds. + Weight::from_parts(18_446_744_073_709_551_000, 0) } // Storage: `ParachainInfo::ParachainId` (r:1 w:0) // Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -120,8 +118,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `107` // Estimated: `3572` - // Minimum execution time: 32_804_000 picoseconds. - Weight::from_parts(33_462_000, 3572) + // Minimum execution time: 31_190_000 picoseconds. + Weight::from_parts(32_150_000, 3572) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -129,8 +127,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 3_921_000 picoseconds. - Weight::from_parts(4_050_000, 0) + // Minimum execution time: 3_603_000 picoseconds. + Weight::from_parts(3_721_000, 0) } // Storage: `System::Account` (r:1 w:1) // Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) @@ -138,8 +136,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `52` // Estimated: `3593` - // Minimum execution time: 25_436_000 picoseconds. - Weight::from_parts(25_789_000, 3593) + // Minimum execution time: 24_265_000 picoseconds. + Weight::from_parts(25_004_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -161,8 +159,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `159` // Estimated: `3624` - // Minimum execution time: 53_846_000 picoseconds. - Weight::from_parts(54_684_000, 3624) + // Minimum execution time: 51_882_000 picoseconds. + Weight::from_parts(53_228_000, 3624) .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -182,8 +180,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `107` // Estimated: `3572` - // Minimum execution time: 33_052_000 picoseconds. - Weight::from_parts(33_897_000, 3572) + // Minimum execution time: 32_195_000 picoseconds. + Weight::from_parts(33_206_000, 3572) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(2)) } diff --git a/polkadot/runtime/rococo/src/weights/xcm/mod.rs b/polkadot/runtime/rococo/src/weights/xcm/mod.rs index 1d613717dc527..cc485dfbaf7e4 100644 --- a/polkadot/runtime/rococo/src/weights/xcm/mod.rs +++ b/polkadot/runtime/rococo/src/weights/xcm/mod.rs @@ -91,7 +91,6 @@ impl<RuntimeCall> XcmWeightInfo<RuntimeCall> for RococoXcmWeight<RuntimeCall> { assets.weigh_multi_assets(XcmBalancesWeight::<Runtime>::withdraw_asset()) } fn reserve_asset_deposited(assets: &MultiAssets) -> Weight { - // Rococo doesn't support ReserveAssetDeposited, so this benchmark has a default weight assets.weigh_multi_assets(XcmBalancesWeight::<Runtime>::reserve_asset_deposited()) } fn receive_teleported_asset(assets: &MultiAssets) -> Weight { diff --git a/polkadot/runtime/rococo/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs b/polkadot/runtime/rococo/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs index 59c49e4f8c821..60c40429b1ac3 100644 --- a/polkadot/runtime/rococo/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +++ b/polkadot/runtime/rococo/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs @@ -17,10 +17,10 @@ //! Autogenerated weights for `pallet_xcm_benchmarks::fungible` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-07-17, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-09-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runner-gghbxkbs-project-163-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("rococo-dev"), DB CACHE: 1024 +//! HOSTNAME: `runner-nbnwcyh-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: Compiled, CHAIN: Some("rococo-dev"), DB CACHE: 1024 // Executed Command: // target/production/polkadot @@ -31,12 +31,12 @@ // --extrinsic=* // --wasm-execution=compiled // --heap-pages=4096 -// --json-file=/builds/parity/mirrors/polkadot/.git/.artifacts/bench.json +// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json // --pallet=pallet_xcm_benchmarks::fungible // --chain=rococo-dev -// --header=./file_header.txt -// --template=./xcm/pallet-xcm-benchmarks/template.hbs -// --output=./runtime/rococo/src/weights/xcm/ +// --header=./polkadot/file_header.txt +// --template=./polkadot/xcm/pallet-xcm-benchmarks/template.hbs +// --output=./polkadot/runtime/rococo/src/weights/xcm/ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -55,8 +55,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `101` // Estimated: `3593` - // Minimum execution time: 24_892_000 picoseconds. - Weight::from_parts(25_219_000, 3593) + // Minimum execution time: 23_189_000 picoseconds. + Weight::from_parts(23_896_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -66,8 +66,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `101` // Estimated: `6196` - // Minimum execution time: 52_112_000 picoseconds. - Weight::from_parts(53_104_000, 6196) + // Minimum execution time: 50_299_000 picoseconds. + Weight::from_parts(50_962_000, 6196) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -83,10 +83,10 @@ impl<T: frame_system::Config> WeightInfo<T> { /// Proof: `Dmp::DownwardMessageQueueHeads` (`max_values`: None, `max_size`: None, mode: `Measured`) pub(crate) fn transfer_reserve_asset() -> Weight { // Proof Size summary in bytes: - // Measured: `210` + // Measured: `243` // Estimated: `6196` - // Minimum execution time: 76_459_000 picoseconds. - Weight::from_parts(79_152_000, 6196) + // Minimum execution time: 71_748_000 picoseconds. + Weight::from_parts(74_072_000, 6196) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(4)) } @@ -96,8 +96,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 2_000_000_000_000 picoseconds. - Weight::from_parts(2_000_000_000_000, 0) + // Minimum execution time: 18_446_744_073_709_551_000 picoseconds. + Weight::from_parts(18_446_744_073_709_551_000, 0) } /// Storage: `Dmp::DeliveryFeeFactor` (r:1 w:0) /// Proof: `Dmp::DeliveryFeeFactor` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -109,10 +109,10 @@ impl<T: frame_system::Config> WeightInfo<T> { /// Proof: `Dmp::DownwardMessageQueueHeads` (`max_values`: None, `max_size`: None, mode: `Measured`) pub(crate) fn initiate_reserve_withdraw() -> Weight { // Proof Size summary in bytes: - // Measured: `109` - // Estimated: `3574` - // Minimum execution time: 29_734_000 picoseconds. - Weight::from_parts(30_651_000, 3574) + // Measured: `142` + // Estimated: `3607` + // Minimum execution time: 27_806_000 picoseconds. + Weight::from_parts(28_594_000, 3607) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -122,8 +122,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `103` // Estimated: `3593` - // Minimum execution time: 23_028_000 picoseconds. - Weight::from_parts(23_687_000, 3593) + // Minimum execution time: 21_199_000 picoseconds. + Weight::from_parts(21_857_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -133,8 +133,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `3593` - // Minimum execution time: 26_399_000 picoseconds. - Weight::from_parts(27_262_000, 3593) + // Minimum execution time: 23_578_000 picoseconds. + Weight::from_parts(24_060_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -150,10 +150,10 @@ impl<T: frame_system::Config> WeightInfo<T> { /// Proof: `Dmp::DownwardMessageQueueHeads` (`max_values`: None, `max_size`: None, mode: `Measured`) pub(crate) fn deposit_reserve_asset() -> Weight { // Proof Size summary in bytes: - // Measured: `109` - // Estimated: `3593` - // Minimum execution time: 52_015_000 picoseconds. - Weight::from_parts(53_498_000, 3593) + // Measured: `142` + // Estimated: `3607` + // Minimum execution time: 48_522_000 picoseconds. + Weight::from_parts(49_640_000, 3607) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -169,10 +169,10 @@ impl<T: frame_system::Config> WeightInfo<T> { /// Proof: `Dmp::DownwardMessageQueueHeads` (`max_values`: None, `max_size`: None, mode: `Measured`) pub(crate) fn initiate_teleport() -> Weight { // Proof Size summary in bytes: - // Measured: `109` - // Estimated: `3593` - // Minimum execution time: 53_833_000 picoseconds. - Weight::from_parts(55_688_000, 3593) + // Measured: `142` + // Estimated: `3607` + // Minimum execution time: 50_429_000 picoseconds. + Weight::from_parts(51_295_000, 3607) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) } diff --git a/polkadot/runtime/westend/src/weights/xcm/mod.rs b/polkadot/runtime/westend/src/weights/xcm/mod.rs index c6fa6bb93eb6a..d5b3d8257ba54 100644 --- a/polkadot/runtime/westend/src/weights/xcm/mod.rs +++ b/polkadot/runtime/westend/src/weights/xcm/mod.rs @@ -94,7 +94,6 @@ impl<RuntimeCall> XcmWeightInfo<RuntimeCall> for WestendXcmWeight<RuntimeCall> { assets.weigh_multi_assets(XcmBalancesWeight::<Runtime>::withdraw_asset()) } fn reserve_asset_deposited(assets: &MultiAssets) -> Weight { - // Westend doesn't support ReserveAssetDeposited, so this benchmark has a default weight assets.weigh_multi_assets(XcmBalancesWeight::<Runtime>::reserve_asset_deposited()) } fn receive_teleported_asset(assets: &MultiAssets) -> Weight { diff --git a/polkadot/runtime/westend/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs b/polkadot/runtime/westend/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs index b92749bfa15b3..87e63fbe31070 100644 --- a/polkadot/runtime/westend/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +++ b/polkadot/runtime/westend/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs @@ -17,10 +17,10 @@ //! Autogenerated weights for `pallet_xcm_benchmarks::fungible` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-07-17, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-09-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runner-gghbxkbs-project-163-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("westend-dev"), DB CACHE: 1024 +//! HOSTNAME: `runner-nbnwcyh-project-674-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: Compiled, CHAIN: Some("westend-dev"), DB CACHE: 1024 // Executed Command: // target/production/polkadot @@ -31,12 +31,12 @@ // --extrinsic=* // --wasm-execution=compiled // --heap-pages=4096 -// --json-file=/builds/parity/mirrors/polkadot/.git/.artifacts/bench.json +// --json-file=/builds/parity/mirrors/polkadot-sdk/.git/.artifacts/bench.json // --pallet=pallet_xcm_benchmarks::fungible // --chain=westend-dev -// --header=./file_header.txt -// --template=./xcm/pallet-xcm-benchmarks/template.hbs -// --output=./runtime/westend/src/weights/xcm/ +// --header=./polkadot/file_header.txt +// --template=./polkadot/xcm/pallet-xcm-benchmarks/template.hbs +// --output=./polkadot/runtime/westend/src/weights/xcm/ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -55,8 +55,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `101` // Estimated: `3593` - // Minimum execution time: 24_887_000 picoseconds. - Weight::from_parts(25_361_000, 3593) + // Minimum execution time: 24_642_000 picoseconds. + Weight::from_parts(24_973_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -66,8 +66,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `101` // Estimated: `6196` - // Minimum execution time: 52_408_000 picoseconds. - Weight::from_parts(53_387_000, 6196) + // Minimum execution time: 50_882_000 picoseconds. + Weight::from_parts(51_516_000, 6196) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -83,10 +83,10 @@ impl<T: frame_system::Config> WeightInfo<T> { /// Proof: `Dmp::DownwardMessageQueueHeads` (`max_values`: None, `max_size`: None, mode: `Measured`) pub(crate) fn transfer_reserve_asset() -> Weight { // Proof Size summary in bytes: - // Measured: `177` + // Measured: `210` // Estimated: `6196` - // Minimum execution time: 74_753_000 picoseconds. - Weight::from_parts(76_838_000, 6196) + // Minimum execution time: 73_923_000 picoseconds. + Weight::from_parts(75_454_000, 6196) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(4)) } @@ -96,8 +96,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 2_000_000_000_000 picoseconds. - Weight::from_parts(2_000_000_000_000, 0) + // Minimum execution time: 18_446_744_073_709_551_000 picoseconds. + Weight::from_parts(18_446_744_073_709_551_000, 0) } /// Storage: `Dmp::DeliveryFeeFactor` (r:1 w:0) /// Proof: `Dmp::DeliveryFeeFactor` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -109,10 +109,10 @@ impl<T: frame_system::Config> WeightInfo<T> { /// Proof: `Dmp::DownwardMessageQueueHeads` (`max_values`: None, `max_size`: None, mode: `Measured`) pub(crate) fn initiate_reserve_withdraw() -> Weight { // Proof Size summary in bytes: - // Measured: `76` - // Estimated: `3541` - // Minimum execution time: 29_272_000 picoseconds. - Weight::from_parts(30_061_000, 3541) + // Measured: `109` + // Estimated: `3574` + // Minimum execution time: 29_035_000 picoseconds. + Weight::from_parts(30_086_000, 3574) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -122,8 +122,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `103` // Estimated: `3593` - // Minimum execution time: 23_112_000 picoseconds. - Weight::from_parts(23_705_000, 3593) + // Minimum execution time: 22_094_000 picoseconds. + Weight::from_parts(22_560_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -133,8 +133,8 @@ impl<T: frame_system::Config> WeightInfo<T> { // Proof Size summary in bytes: // Measured: `0` // Estimated: `3593` - // Minimum execution time: 26_077_000 picoseconds. - Weight::from_parts(26_486_000, 3593) + // Minimum execution time: 24_771_000 picoseconds. + Weight::from_parts(25_280_000, 3593) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -150,10 +150,10 @@ impl<T: frame_system::Config> WeightInfo<T> { /// Proof: `Dmp::DownwardMessageQueueHeads` (`max_values`: None, `max_size`: None, mode: `Measured`) pub(crate) fn deposit_reserve_asset() -> Weight { // Proof Size summary in bytes: - // Measured: `76` + // Measured: `109` // Estimated: `3593` - // Minimum execution time: 51_022_000 picoseconds. - Weight::from_parts(52_498_000, 3593) + // Minimum execution time: 49_777_000 picoseconds. + Weight::from_parts(50_833_000, 3593) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -169,10 +169,10 @@ impl<T: frame_system::Config> WeightInfo<T> { /// Proof: `Dmp::DownwardMessageQueueHeads` (`max_values`: None, `max_size`: None, mode: `Measured`) pub(crate) fn initiate_teleport() -> Weight { // Proof Size summary in bytes: - // Measured: `76` + // Measured: `109` // Estimated: `3593` - // Minimum execution time: 53_062_000 picoseconds. - Weight::from_parts(54_300_000, 3593) + // Minimum execution time: 51_425_000 picoseconds. + Weight::from_parts(52_213_000, 3593) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) } diff --git a/polkadot/xcm/pallet-xcm-benchmarks/src/fungible/benchmarking.rs b/polkadot/xcm/pallet-xcm-benchmarks/src/fungible/benchmarking.rs index 504b795403991..760fa33b693e5 100644 --- a/polkadot/xcm/pallet-xcm-benchmarks/src/fungible/benchmarking.rs +++ b/polkadot/xcm/pallet-xcm-benchmarks/src/fungible/benchmarking.rs @@ -20,6 +20,7 @@ use frame_benchmarking::{benchmarks_instance_pallet, BenchmarkError, BenchmarkRe use frame_support::{ pallet_prelude::Get, traits::fungible::{Inspect, Mutate}, + weights::Weight, }; use sp_runtime::traits::{Bounded, Zero}; use sp_std::{prelude::*, vec}; @@ -134,7 +135,7 @@ benchmarks_instance_pallet! { reserve_asset_deposited { let (trusted_reserve, transferable_reserve_asset) = T::TrustedReserve::get() .ok_or(BenchmarkError::Override( - BenchmarkResult::from_weight(T::BlockWeights::get().max_block) + BenchmarkResult::from_weight(Weight::MAX) ))?; let assets: MultiAssets = vec![ transferable_reserve_asset ].into(); @@ -187,7 +188,7 @@ benchmarks_instance_pallet! { }: { executor.bench_process(xcm).map_err(|_| { BenchmarkError::Override( - BenchmarkResult::from_weight(T::BlockWeights::get().max_block) + BenchmarkResult::from_weight(Weight::MAX) ) })?; } verify { From 64877492c5680af48cb97e0b4d1e3801a18353f0 Mon Sep 17 00:00:00 2001 From: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Date: Tue, 10 Oct 2023 16:02:35 +0200 Subject: [PATCH 23/54] [FRAME] Warn on unchecked weight witness (#1818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a warning to FRAME pallets when a function argument that starts with `_` is used in the weight formula. This is in most cases an error since the weight witness needs to be checked. Example: ```rust #[pallet::call_index(0)] #[pallet::weight(T::SystemWeightInfo::remark(_remark.len() as u32))] pub fn remark(_origin: OriginFor<T>, _remark: Vec<u8>) -> DispatchResultWithPostInfo { Ok(().into()) } ``` Produces this warning: ```pre warning: use of deprecated constant `pallet::warnings::UncheckedWeightWitness_0::_w`: It is deprecated to not check weight witness data. Please instead ensure that all witness data for weight calculation is checked before usage. For more info see: <https://github.com/paritytech/polkadot-sdk/pull/1818> --> substrate/frame/system/src/lib.rs:424:40 | 424 | pub fn remark(_origin: OriginFor<T>, _remark: Vec<u8>) -> DispatchResultWithPostInfo { | ^^^^^^^ | = note: `#[warn(deprecated)]` on by default ``` Can be suppressed like this, since in this case it is legit: ```rust #[pallet::call_index(0)] #[pallet::weight(T::SystemWeightInfo::remark(remark.len() as u32))] pub fn remark(_origin: OriginFor<T>, remark: Vec<u8>) -> DispatchResultWithPostInfo { let _ = remark; // We dont need to check the weight witness. Ok(().into()) } ``` Changes: - Add warning on uncheded weight witness - Respect `subkeys` limit in `System::kill_prefix` - Fix HRMP pallet and other warnings - Update`proc_macro_warning` dependency - Delete random folder `substrate/src/src` 🙈 - Adding Prdoc --------- Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> --- Cargo.lock | 8 +- polkadot/runtime/parachains/src/hrmp.rs | 40 ++- prdoc/pr_1818.prdoc | 16 + .../elections-phragmen/src/benchmarking.rs | 2 +- substrate/frame/elections-phragmen/src/lib.rs | 9 +- substrate/frame/root-testing/src/lib.rs | 2 +- substrate/frame/sudo/src/lib.rs | 5 +- substrate/frame/support/procedural/Cargo.toml | 2 +- .../procedural/src/construct_runtime/mod.rs | 2 +- .../procedural/src/pallet/expand/call.rs | 20 +- .../procedural/src/pallet/expand/mod.rs | 1 + .../procedural/src/pallet/expand/warnings.rs | 102 ++++++ substrate/frame/support/test/tests/pallet.rs | 5 +- .../support/test/tests/pallet_instance.rs | 5 +- .../call_weight_unchecked_warning.rs | 38 +++ .../call_weight_unchecked_warning.stderr | 12 + substrate/frame/system/src/lib.rs | 11 +- substrate/frame/utility/src/lib.rs | 6 +- substrate/src/src/lib.rs | 297 ------------------ 19 files changed, 243 insertions(+), 340 deletions(-) create mode 100644 prdoc/pr_1818.prdoc create mode 100644 substrate/frame/support/procedural/src/pallet/expand/warnings.rs create mode 100644 substrate/frame/support/test/tests/pallet_ui/call_weight_unchecked_warning.rs create mode 100644 substrate/frame/support/test/tests/pallet_ui/call_weight_unchecked_warning.stderr delete mode 100644 substrate/src/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index cc80e2542fb06..cc8415a5a23c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13327,9 +13327,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro-warning" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1eaa7fa0aa1929ffdf7eeb6eac234dde6268914a14ad44d23521ab6a9b258e" +checksum = "9b698b0b09d40e9b7c1a47b132d66a8b54bcd20583d9b6d06e4535e383b4405c" dependencies = [ "proc-macro2", "quote", @@ -13338,9 +13338,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" dependencies = [ "unicode-ident", ] diff --git a/polkadot/runtime/parachains/src/hrmp.rs b/polkadot/runtime/parachains/src/hrmp.rs index b3bbcb433c0b2..42592d9d9f149 100644 --- a/polkadot/runtime/parachains/src/hrmp.rs +++ b/polkadot/runtime/parachains/src/hrmp.rs @@ -554,14 +554,26 @@ pub mod pallet { /// /// Origin must be the `ChannelManager`. #[pallet::call_index(3)] - #[pallet::weight(<T as Config>::WeightInfo::force_clean_hrmp(*_inbound, *_outbound))] + #[pallet::weight(<T as Config>::WeightInfo::force_clean_hrmp(*num_inbound, *num_outbound))] pub fn force_clean_hrmp( origin: OriginFor<T>, para: ParaId, - _inbound: u32, - _outbound: u32, + num_inbound: u32, + num_outbound: u32, ) -> DispatchResult { T::ChannelManager::ensure_origin(origin)?; + + ensure!( + HrmpIngressChannelsIndex::<T>::decode_len(para).unwrap_or_default() <= + num_inbound as usize, + Error::<T>::WrongWitness + ); + ensure!( + HrmpEgressChannelsIndex::<T>::decode_len(para).unwrap_or_default() <= + num_outbound as usize, + Error::<T>::WrongWitness + ); + Self::clean_hrmp_after_outgoing(¶); Ok(()) } @@ -575,9 +587,16 @@ pub mod pallet { /// /// Origin must be the `ChannelManager`. #[pallet::call_index(4)] - #[pallet::weight(<T as Config>::WeightInfo::force_process_hrmp_open(*_channels))] - pub fn force_process_hrmp_open(origin: OriginFor<T>, _channels: u32) -> DispatchResult { + #[pallet::weight(<T as Config>::WeightInfo::force_process_hrmp_open(*channels))] + pub fn force_process_hrmp_open(origin: OriginFor<T>, channels: u32) -> DispatchResult { T::ChannelManager::ensure_origin(origin)?; + + ensure!( + HrmpOpenChannelRequestsList::<T>::decode_len().unwrap_or_default() as u32 <= + channels, + Error::<T>::WrongWitness + ); + let host_config = configuration::Pallet::<T>::config(); Self::process_hrmp_open_channel_requests(&host_config); Ok(()) @@ -592,9 +611,16 @@ pub mod pallet { /// /// Origin must be the `ChannelManager`. #[pallet::call_index(5)] - #[pallet::weight(<T as Config>::WeightInfo::force_process_hrmp_close(*_channels))] - pub fn force_process_hrmp_close(origin: OriginFor<T>, _channels: u32) -> DispatchResult { + #[pallet::weight(<T as Config>::WeightInfo::force_process_hrmp_close(*channels))] + pub fn force_process_hrmp_close(origin: OriginFor<T>, channels: u32) -> DispatchResult { T::ChannelManager::ensure_origin(origin)?; + + ensure!( + HrmpCloseChannelRequestsList::<T>::decode_len().unwrap_or_default() as u32 <= + channels, + Error::<T>::WrongWitness + ); + Self::process_hrmp_close_channel_requests(); Ok(()) } diff --git a/prdoc/pr_1818.prdoc b/prdoc/pr_1818.prdoc new file mode 100644 index 0000000000000..cbafa02f9af56 --- /dev/null +++ b/prdoc/pr_1818.prdoc @@ -0,0 +1,16 @@ +title: FRAME pallets warning for unchecked weight witness + +doc: + - audience: Core Dev + description: | + FRAME pallets now emit a warning when a call uses a function argument that starts with an underscore in its weight declaration. + +migrations: + db: [ ] + runtime: [ ] + +host_functions: [] + +crates: +- name: "frame-support-procedural" + semver: minor diff --git a/substrate/frame/elections-phragmen/src/benchmarking.rs b/substrate/frame/elections-phragmen/src/benchmarking.rs index 56ea19578c8f5..9878f7fd41c06 100644 --- a/substrate/frame/elections-phragmen/src/benchmarking.rs +++ b/substrate/frame/elections-phragmen/src/benchmarking.rs @@ -379,7 +379,7 @@ benchmarks! { let root = RawOrigin::Root; }: _(root, v, d) verify { - assert_eq!(<Voting<T>>::iter().count() as u32, 0); + assert_eq!(<Voting<T>>::iter().count() as u32, v - d); } election_phragmen { diff --git a/substrate/frame/elections-phragmen/src/lib.rs b/substrate/frame/elections-phragmen/src/lib.rs index 6912649bd122d..93f9fc2b6d241 100644 --- a/substrate/frame/elections-phragmen/src/lib.rs +++ b/substrate/frame/elections-phragmen/src/lib.rs @@ -591,15 +591,18 @@ pub mod pallet { /// ## Complexity /// - Check is_defunct_voter() details. #[pallet::call_index(5)] - #[pallet::weight(T::WeightInfo::clean_defunct_voters(*_num_voters, *_num_defunct))] + #[pallet::weight(T::WeightInfo::clean_defunct_voters(*num_voters, *num_defunct))] pub fn clean_defunct_voters( origin: OriginFor<T>, - _num_voters: u32, - _num_defunct: u32, + num_voters: u32, + num_defunct: u32, ) -> DispatchResult { let _ = ensure_root(origin)?; + <Voting<T>>::iter() + .take(num_voters as usize) .filter(|(_, x)| Self::is_defunct_voter(&x.votes)) + .take(num_defunct as usize) .for_each(|(dv, _)| Self::do_remove_voter(&dv)); Ok(()) diff --git a/substrate/frame/root-testing/src/lib.rs b/substrate/frame/root-testing/src/lib.rs index e04c7bfa13d26..bbcda09c3065d 100644 --- a/substrate/frame/root-testing/src/lib.rs +++ b/substrate/frame/root-testing/src/lib.rs @@ -29,7 +29,7 @@ use sp_runtime::Perbill; pub use pallet::*; -#[frame_support::pallet] +#[frame_support::pallet(dev_mode)] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; diff --git a/substrate/frame/sudo/src/lib.rs b/substrate/frame/sudo/src/lib.rs index 0c869bec7f076..fb29c0da42a99 100644 --- a/substrate/frame/sudo/src/lib.rs +++ b/substrate/frame/sudo/src/lib.rs @@ -204,14 +204,15 @@ pub mod pallet { /// ## Complexity /// - O(1). #[pallet::call_index(1)] - #[pallet::weight((*_weight, call.get_dispatch_info().class))] + #[pallet::weight((*weight, call.get_dispatch_info().class))] pub fn sudo_unchecked_weight( origin: OriginFor<T>, call: Box<<T as Config>::RuntimeCall>, - _weight: Weight, + weight: Weight, ) -> DispatchResultWithPostInfo { // This is a public call, so we ensure that the origin is some signed account. let sender = ensure_signed(origin)?; + let _ = weight; // We don't check the weight witness since it is a root call. ensure!(Self::key().map_or(false, |k| sender == k), Error::<T>::RequireSudo); let res = call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); diff --git a/substrate/frame/support/procedural/Cargo.toml b/substrate/frame/support/procedural/Cargo.toml index d2854a2a79f00..9781a88251428 100644 --- a/substrate/frame/support/procedural/Cargo.toml +++ b/substrate/frame/support/procedural/Cargo.toml @@ -23,7 +23,7 @@ proc-macro2 = "1.0.56" quote = "1.0.28" syn = { version = "2.0.38", features = ["full"] } frame-support-procedural-tools = { path = "tools" } -proc-macro-warning = { version = "0.4.2", default-features = false } +proc-macro-warning = { version = "1.0.0", default-features = false } macro_magic = { version = "0.4.2", features = ["proc_support"] } expander = "2.0.0" sp-core-hashing = { path = "../../../primitives/core/hashing" } diff --git a/substrate/frame/support/procedural/src/construct_runtime/mod.rs b/substrate/frame/support/procedural/src/construct_runtime/mod.rs index e8c3c0889214c..c3d433643fd7e 100644 --- a/substrate/frame/support/procedural/src/construct_runtime/mod.rs +++ b/substrate/frame/support/procedural/src/construct_runtime/mod.rs @@ -414,7 +414,7 @@ fn construct_runtime_final_expansion( ) .help_links(&["https://github.com/paritytech/substrate/pull/14437"]) .span(where_section.span) - .build(), + .build_or_panic(), ) }); diff --git a/substrate/frame/support/procedural/src/pallet/expand/call.rs b/substrate/frame/support/procedural/src/pallet/expand/call.rs index 3ed5509863e91..ed6335159cd6e 100644 --- a/substrate/frame/support/procedural/src/pallet/expand/call.rs +++ b/substrate/frame/support/procedural/src/pallet/expand/call.rs @@ -17,12 +17,14 @@ use crate::{ pallet::{ + expand::warnings::{weight_constant_warning, weight_witness_warning}, parse::call::{CallVariantDef, CallWeightDef}, Def, }, COUNTER, }; use proc_macro2::TokenStream as TokenStream2; +use proc_macro_warning::Warning; use quote::{quote, ToTokens}; use syn::spanned::Spanned; @@ -68,7 +70,7 @@ pub fn expand_call(def: &mut Def) -> proc_macro2::TokenStream { continue } - let warning = proc_macro_warning::Warning::new_deprecated("ImplicitCallIndex") + let warning = Warning::new_deprecated("ImplicitCallIndex") .index(call_index_warnings.len()) .old("use implicit call indices") .new("ensure that all calls have a `pallet::call_index` attribute or put the pallet into `dev` mode") @@ -77,7 +79,7 @@ pub fn expand_call(def: &mut Def) -> proc_macro2::TokenStream { "https://github.com/paritytech/substrate/pull/11381" ]) .span(method.name.span()) - .build(); + .build_or_panic(); call_index_warnings.push(warning); } @@ -86,18 +88,12 @@ pub fn expand_call(def: &mut Def) -> proc_macro2::TokenStream { for method in &methods { match &method.weight { CallWeightDef::DevModeDefault => fn_weight.push(syn::parse_quote!(0)), - CallWeightDef::Immediate(e @ syn::Expr::Lit(lit)) if !def.dev_mode => { - let warning = proc_macro_warning::Warning::new_deprecated("ConstantWeight") - .index(weight_warnings.len()) - .old("use hard-coded constant as call weight") - .new("benchmark all calls or put the pallet into `dev` mode") - .help_link("https://github.com/paritytech/substrate/pull/13798") - .span(lit.span()) - .build(); - weight_warnings.push(warning); + CallWeightDef::Immediate(e) => { + weight_constant_warning(e, def.dev_mode, &mut weight_warnings); + weight_witness_warning(method, def.dev_mode, &mut weight_warnings); + fn_weight.push(e.into_token_stream()); }, - CallWeightDef::Immediate(e) => fn_weight.push(e.into_token_stream()), CallWeightDef::Inherited => { let pallet_weight = def .call diff --git a/substrate/frame/support/procedural/src/pallet/expand/mod.rs b/substrate/frame/support/procedural/src/pallet/expand/mod.rs index 2b998227c1d84..6f32e5697512f 100644 --- a/substrate/frame/support/procedural/src/pallet/expand/mod.rs +++ b/substrate/frame/support/procedural/src/pallet/expand/mod.rs @@ -34,6 +34,7 @@ mod store_trait; mod tt_default_parts; mod type_value; mod validate_unsigned; +mod warnings; use crate::pallet::Def; use quote::ToTokens; diff --git a/substrate/frame/support/procedural/src/pallet/expand/warnings.rs b/substrate/frame/support/procedural/src/pallet/expand/warnings.rs new file mode 100644 index 0000000000000..ae5890878a2f6 --- /dev/null +++ b/substrate/frame/support/procedural/src/pallet/expand/warnings.rs @@ -0,0 +1,102 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// 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. + +//! Generates warnings for undesirable pallet code. + +use crate::pallet::parse::call::{CallVariantDef, CallWeightDef}; +use proc_macro_warning::Warning; +use syn::{ + spanned::Spanned, + visit::{self, Visit}, +}; + +/// Warn if any of the call arguments starts with a underscore and is used in a weight formula. +pub(crate) fn weight_witness_warning( + method: &CallVariantDef, + dev_mode: bool, + warnings: &mut Vec<Warning>, +) { + if dev_mode { + return + } + let CallWeightDef::Immediate(w) = &method.weight else { + return; + }; + + let partial_warning = Warning::new_deprecated("UncheckedWeightWitness") + .old("not check weight witness data") + .new("ensure that all witness data for weight calculation is checked before usage") + .help_link("https://github.com/paritytech/polkadot-sdk/pull/1818"); + + for (_, arg_ident, _) in method.args.iter() { + if !arg_ident.to_string().starts_with('_') || !contains_ident(w.clone(), &arg_ident) { + continue + } + + let warning = partial_warning + .clone() + .index(warnings.len()) + .span(arg_ident.span()) + .build_or_panic(); + + warnings.push(warning); + } +} + +/// Warn if the weight is a constant and the pallet not in `dev_mode`. +pub(crate) fn weight_constant_warning( + weight: &syn::Expr, + dev_mode: bool, + warnings: &mut Vec<Warning>, +) { + if dev_mode { + return + } + let syn::Expr::Lit(lit) = weight else { + return; + }; + + let warning = Warning::new_deprecated("ConstantWeight") + .index(warnings.len()) + .old("use hard-coded constant as call weight") + .new("benchmark all calls or put the pallet into `dev` mode") + .help_link("https://github.com/paritytech/substrate/pull/13798") + .span(lit.span()) + .build_or_panic(); + + warnings.push(warning); +} + +/// Returns whether `expr` contains `ident`. +fn contains_ident(mut expr: syn::Expr, ident: &syn::Ident) -> bool { + struct ContainsIdent { + ident: syn::Ident, + found: bool, + } + + impl<'a> Visit<'a> for ContainsIdent { + fn visit_ident(&mut self, i: &syn::Ident) { + if *i == self.ident { + self.found = true; + } + } + } + + let mut visitor = ContainsIdent { ident: ident.clone(), found: false }; + visit::visit_expr(&mut visitor, &mut expr); + visitor.found +} diff --git a/substrate/frame/support/test/tests/pallet.rs b/substrate/frame/support/test/tests/pallet.rs index 1898246470c73..83ae5b9253cec 100644 --- a/substrate/frame/support/test/tests/pallet.rs +++ b/substrate/frame/support/test/tests/pallet.rs @@ -210,12 +210,13 @@ pub mod pallet { { /// Doc comment put in metadata #[pallet::call_index(0)] - #[pallet::weight(Weight::from_parts(*_foo as u64, 0))] + #[pallet::weight(Weight::from_parts(*foo as u64, 0))] pub fn foo( origin: OriginFor<T>, - #[pallet::compact] _foo: u32, + #[pallet::compact] foo: u32, _bar: u32, ) -> DispatchResultWithPostInfo { + let _ = foo; let _ = T::AccountId::from(SomeType1); // Test for where clause let _ = T::AccountId::from(SomeType3); // Test for where clause let _ = origin; diff --git a/substrate/frame/support/test/tests/pallet_instance.rs b/substrate/frame/support/test/tests/pallet_instance.rs index 8d2d52d18852a..724734ec4fc9d 100644 --- a/substrate/frame/support/test/tests/pallet_instance.rs +++ b/substrate/frame/support/test/tests/pallet_instance.rs @@ -87,12 +87,13 @@ pub mod pallet { impl<T: Config<I>, I: 'static> Pallet<T, I> { /// Doc comment put in metadata #[pallet::call_index(0)] - #[pallet::weight(Weight::from_parts(*_foo as u64, 0))] + #[pallet::weight(Weight::from_parts(*foo as u64, 0))] pub fn foo( origin: OriginFor<T>, - #[pallet::compact] _foo: u32, + #[pallet::compact] foo: u32, ) -> DispatchResultWithPostInfo { let _ = origin; + let _ = foo; Self::deposit_event(Event::Something(3)); Ok(().into()) } diff --git a/substrate/frame/support/test/tests/pallet_ui/call_weight_unchecked_warning.rs b/substrate/frame/support/test/tests/pallet_ui/call_weight_unchecked_warning.rs new file mode 100644 index 0000000000000..8d93638f5a51e --- /dev/null +++ b/substrate/frame/support/test/tests/pallet_ui/call_weight_unchecked_warning.rs @@ -0,0 +1,38 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// 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. + +#[frame_support::pallet] +mod pallet { + use frame_support::pallet_prelude::DispatchResult; + use frame_system::pallet_prelude::OriginFor; + + #[pallet::config] + pub trait Config: frame_system::Config {} + + #[pallet::pallet] + pub struct Pallet<T>(core::marker::PhantomData<T>); + + #[pallet::call] + impl<T: Config> Pallet<T> { + #[pallet::call_index(0)] + #[pallet::weight(*_unused)] + pub fn foo(_: OriginFor<T>, _unused: u64) -> DispatchResult { Ok(()) } + } +} + +fn main() { +} diff --git a/substrate/frame/support/test/tests/pallet_ui/call_weight_unchecked_warning.stderr b/substrate/frame/support/test/tests/pallet_ui/call_weight_unchecked_warning.stderr new file mode 100644 index 0000000000000..89fc1e0820f5e --- /dev/null +++ b/substrate/frame/support/test/tests/pallet_ui/call_weight_unchecked_warning.stderr @@ -0,0 +1,12 @@ +error: use of deprecated constant `pallet::warnings::UncheckedWeightWitness_0::_w`: + It is deprecated to not check weight witness data. + Please instead ensure that all witness data for weight calculation is checked before usage. + + For more info see: + <https://github.com/paritytech/polkadot-sdk/pull/1818> + --> tests/pallet_ui/call_weight_unchecked_warning.rs:33:31 + | +33 | pub fn foo(_: OriginFor<T>, _unused: u64) -> DispatchResult { Ok(()) } + | ^^^^^^^ + | + = note: `-D deprecated` implied by `-D warnings` diff --git a/substrate/frame/system/src/lib.rs b/substrate/frame/system/src/lib.rs index 84b6dc031457d..897d3bd7ce91f 100644 --- a/substrate/frame/system/src/lib.rs +++ b/substrate/frame/system/src/lib.rs @@ -420,8 +420,9 @@ pub mod pallet { /// /// Can be executed by every `origin`. #[pallet::call_index(0)] - #[pallet::weight(T::SystemWeightInfo::remark(_remark.len() as u32))] - pub fn remark(_origin: OriginFor<T>, _remark: Vec<u8>) -> DispatchResultWithPostInfo { + #[pallet::weight(T::SystemWeightInfo::remark(remark.len() as u32))] + pub fn remark(_origin: OriginFor<T>, remark: Vec<u8>) -> DispatchResultWithPostInfo { + let _ = remark; // No need to check the weight witness. Ok(().into()) } @@ -495,16 +496,16 @@ pub mod pallet { /// the prefix we are removing to accurately calculate the weight of this function. #[pallet::call_index(6)] #[pallet::weight(( - T::SystemWeightInfo::kill_prefix(_subkeys.saturating_add(1)), + T::SystemWeightInfo::kill_prefix(subkeys.saturating_add(1)), DispatchClass::Operational, ))] pub fn kill_prefix( origin: OriginFor<T>, prefix: Key, - _subkeys: u32, + subkeys: u32, ) -> DispatchResultWithPostInfo { ensure_root(origin)?; - let _ = storage::unhashed::clear_prefix(&prefix, None, None); + let _ = storage::unhashed::clear_prefix(&prefix, Some(subkeys), None); Ok(().into()) } diff --git a/substrate/frame/utility/src/lib.rs b/substrate/frame/utility/src/lib.rs index af212a31eb971..7f963e3637d6f 100644 --- a/substrate/frame/utility/src/lib.rs +++ b/substrate/frame/utility/src/lib.rs @@ -479,13 +479,15 @@ pub mod pallet { /// /// The dispatch origin for this call must be _Root_. #[pallet::call_index(5)] - #[pallet::weight((*_weight, call.get_dispatch_info().class))] + #[pallet::weight((*weight, call.get_dispatch_info().class))] pub fn with_weight( origin: OriginFor<T>, call: Box<<T as Config>::RuntimeCall>, - _weight: Weight, + weight: Weight, ) -> DispatchResult { ensure_root(origin)?; + let _ = weight; // Explicitly don't check the the weight witness. + let res = call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); res.map(|_| ()).map_err(|e| e.error) } diff --git a/substrate/src/src/lib.rs b/substrate/src/src/lib.rs deleted file mode 100644 index 16a6067789657..0000000000000 --- a/substrate/src/src/lib.rs +++ /dev/null @@ -1,297 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -//! # Substrate -//! -//! Substrate is a Rust framework for building blockchains in a modular and extensible way. While in -//! itself un-opinionated, it is the main engine behind the Polkadot ecosystem. -//! -//! [![github]](https://github.com/paritytech/substrate/) - [![polkadot]](https://polkadot.network) -//! -//! This crate in itself does not contain any code and is just meant ot be a documentation hub for -//! substrate-based crates. -//! -//! ## Overview -//! -//! Substrate approaches blockchain development with an acknowledgement of a few self-evident -//! truths: -//! -//! 1. Society and technology evolves. -//! 2. Humans are fallible. -//! -//! This, specifically, makes the task of designing a correct, safe and long-lasting blockchain -//! system hard. -//! -//! Nonetheless, in order to achieve this goal, substrate embraces the following: -//! -//! 1. Use of **Rust** as a modern, and safe programming language, which limits human error through -//! various means, most notably memory safety. -//! 2. Substrate is written from the ground-up with a generic, modular and extensible design. This -//! ensures that software components can be easily swapped and upgraded. Examples of this is -//! multiple consensus mechanisms provided by Substrate, as listed below. -//! 3. Lastly, the final blockchain system created with the above properties needs to be -//! upgradeable. In order to achieve this, Substrate is designed as a meta-protocol, whereby the -//! application logic of the blockchain (called "Runtime") is encoded as a Wasm blob, and is -//! stored onchain. The rest of the system (called "Client") acts as the executor of the Wasm -//! blob. -//! -//! In essence, the meta-protocol of all Substrate based chains is the "Runtime as Wasm blob" -//! accord. This enables the Runtime to become inherently upgradeable (without forks). The upgrade -//! is merely a matter of the Wasm blob being changed in the chain state, which is, in principle, -//! same as updating an account's balance. -//! -//! To learn more about the substrate architecture using some visuals, see [`substrate_diagram`]. -//! -//! `FRAME`, Substrate's default runtime development library takes the above even further by -//! embracing a declarative programming model whereby correctness is enhanced and the system is -//! highly configurable through parameterization. -//! -//! All in all, this design enables all substrate-based chains to achieve forkless, self-enacting -//! upgrades out of the box. Combined with governance abilities that are shipped with `FRAME`, this -//! enables a chain to survive the test of time. -//! -//! ## How to Get Stared -//! -//! Most developers want to leave the client side code as-is, and focus on the runtime. To do so, -//! look into the [`frame_support`] crate, which is the entry point crate into runtime development -//! with FRAME. -//! -//! > Side note, it is entirely possible to craft a substrate-based runtime without FRAME, an -//! > example of which can be found [here](https://github.com/JoshOrndorff/frameless-node-template). -//! -//! In more broad terms, the following avenues exist into developing with substrate: -//! -//! * **Templates**: A number of substrate-based templates exist and they can be used for various -//! purposes, with zero to little additional code needed. All of these templates contain runtimes -//! that are highly configurable and are likely suitable for basic needs. -//! * `FRAME`: If need, one can customize that runtime even further, by using `FRAME` and developing -//! custom modules. -//! * **Core**: To the contrary, some developers may want to customize the client side software to -//! achieve novel goals such as a new consensus engine, or a new database backend. While -//! Substrate's main configurability is in the runtime, the client is also highly generic and can -//! be customized to a great extent. -//! -//! ## Structure -//! -//! Substrate is a massive cargo workspace with hundreds of crates, therefore it is useful to know -//! how to navigate its crates. -//! -//! In broad terms, it is divided into three categories: -//! -//! * `sc-*` (short for *substrate-client*) crates, located under `./client` folder. These are all -//! the client crates. Notable examples are crates such as [`sc-network`], various consensus -//! crates, [`sc-rpc-api`] and [`sc-client-db`], all of which are expected to reside in the client -//! side. -//! * `sp-*` (short for *substrate-primitives*) crates, located under `./primitives` folder. These -//! are the traits that glue the client and runtime together, but are not opinionated about what -//! framework is using for building the runtime. Notable examples are [`sp-api`] and [`sp-io`], -//! which form the communication bridge between the client and runtime, as explained in -//! [`substrate_diagram`]. -//! * `pallet-*` and `frame-*` crates, located under `./frame` folder. These are the crates related -//! to FRAME. See [`frame_support`] for more information. -//! -//! ### Wasm Build -//! -//! Many of the Substrate crates, such as entire `sp-*`, need to compile to both Wasm (when a Wasm -//! runtime is being generated) and native (for example, when testing). To achieve this, Substrate -//! follows the convention of the Rust community, and uses a `feature = "std"` to signify that a -//! crate is being built with the standard library, and is built for native. Otherwise, it is built -//! for `no_std`. -//! -//! This can be summarized in `#![cfg_attr(not(feature = "std"), no_std)]`, which you can often find -//! in any Substrate-based runtime. -//! -//! Substrate-based runtimes use [`substrate-wasm-builder`] in their `build.rs` to automatically -//! build their Wasm files as a part of normal build commandsOnce built, the wasm file is placed in -//! `./target/{debug|release}/wbuild/{runtime_name}.wasm`. -//! -//! ### Binaries -//! -//! Multiple binaries are shipped with substrate, the most important of which are located in the -//! `./bin` folder. -//! -//! * [`node`] is an extensive substrate node that contains the superset of all runtime and client -//! side features. The corresponding runtime, called [`kitchensink_runtime`] contains all of the -//! modules that are provided with `FRAME`. This node and runtime is only used for testing and -//! demonstration. -//! * [`chain-spec-builder`]: Utility to build more detailed chain-specs for the aforementioned -//! node. Other projects typically contain a `build-spec` subcommand that does the same. -//! * [`node-template`]: a template node that contains a minimal set of features and can act as a -//! starting point of a project. -//! * [`subkey`]: Substrate's key management utility. -//! -//! ### Anatomy of a Binary Crate -//! -//! From the above, [`node`] and [`node-template`] are essentially blueprints of a substrate-based -//! project, as the name of the latter is implying. Each substrate-based project typically contains -//! the following: -//! -//! * Under `./runtime`, a `./runtime/src/lib.rs` which is the top level runtime amalgamator file. -//! This file typically contains the [`frame_support::construct_runtime`] macro, which is the -//! final definition of a runtime. -//! -//! * Under `./node`, a `main.rs`, which is the point, and a `./service.rs`, which contains all the -//! client side components. Skimming this file yields an overview of the networking, database, -//! consensus and similar client side components. -//! -//! > The above two are conventions, not rules. -//! -//! ## Parachain? -//! -//! As noted above, Substrate is the main engine behind the Polkadot ecosystem. One of the ways -//! through which Polkadot can be utilized is by building "parachains", blockchains that are -//! connected to Polkadot's shared security. -//! -//! To build a parachain, one could use [`Cumulus`](https://github.com/paritytech/cumulus/), the -//! library on top of Substrate, empowering any substrate-based chain to be a Polkadot parachain. -//! -//! ## Where To Go Next? -//! -//! Additional noteworthy crates within substrate: -//! -//! - RPC APIs of a Substrate node: [`sc-rpc-api`]/[`sc-rpc`] -//! - CLI Options of a Substrate node: [`sc-cli`] -//! - All of the consensus related crates provided by Substrate: -//! - [`sc-consensus-aura`] -//! - [`sc-consensus-babe`] -//! - [`sc-consensus-grandpa`] -//! - [`sc-consensus-beefy`] -//! - [`sc-consensus-manual-seal`] -//! - [`sc-consensus-pow`] -//! -//! Additional noteworthy external resources: -//! -//! - [Substrate Developer Hub](https://substrate.dev) -//! - [Parity Tech's Documentation Hub](https://paritytech.github.io/) -//! - [Frontier: Substrate's Ethereum Compatibility Library](https://paritytech.github.io/frontier/) -//! - [Polkadot Wiki](https://wiki.polkadot.network/en/) -//! -//! Notable upstream crates: -//! -//! - [`parity-scale-codec`](https://github.com/paritytech/parity-scale-codec) -//! - [`parity-db`](https://github.com/paritytech/parity-db) -//! - [`trie`](https://github.com/paritytech/trie) -//! - [`parity-common`](https://github.com/paritytech/parity-common) -//! -//! Templates: -//! -//! - classic [`substrate-node-template`](https://github.com/substrate-developer-hub/substrate-node-template) -//! - classic [cumulus-parachain-template](https://github.com/substrate-developer-hub/substrate-parachain-template) -//! - [`extended-parachain-template`](https://github.com/paritytech/extended-parachain-template) -//! - [`frontier-parachain-template`](https://github.com/paritytech/frontier-parachain-template) -//! -//! [polkadot]: -//! https://img.shields.io/badge/polkadot-E6007A?style=for-the-badge&logo=polkadot&logoColor=white -//! [github]: -//! https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github -//! [`sp-io`]: ../sp_io/index.html -//! [`sp-api`]: ../sp_api/index.html -//! [`sp-api`]: ../sp_api/index.html -//! [`sc-client-db`]: ../sc_client_db/index.html -//! [`sc-network`]: ../sc_network/index.html -//! [`sc-rpc-api`]: ../sc_rpc_api/index.html -//! [`sc-rpc`]: ../sc_rpc/index.html -//! [`sc-cli`]: ../sc_cli/index.html -//! [`sc-consensus-aura`]: ../sc_consensus_aura/index.html -//! [`sc-consensus-babe`]: ../sc_consensus_babe/index.html -//! [`sc-consensus-grandpa`]: ../sc_consensus_grandpa/index.html -//! [`sc-consensus-beefy`]: ../sc_consensus_beefy/index.html -//! [`sc-consensus-manual-seal`]: ../sc_consensus_manual_seal/index.html -//! [`sc-consensus-pow`]: ../sc_consensus_pow/index.html -//! [`node`]: ../node_cli/index.html -//! [`node-template`]: ../node_template/index.html -//! [`kitchensink_runtime`]: ../kitchensink_runtime/index.html -//! [`subkey`]: ../subkey/index.html -//! [`chain-spec-builder`]: ../chain_spec_builder/index.html -//! [`substrate-wasm-builder`]: https://crates.io/crates/substrate-wasm-builder - -#![deny(rustdoc::broken_intra_doc_links)] -#![deny(rustdoc::private_intra_doc_links)] - -#[cfg_attr(doc, aquamarine::aquamarine)] -/// In this module, we explore substrate at a more depth. First, let's establish substrate being -/// divided into a client and runtime. -/// -/// ```mermaid -/// graph TB -/// subgraph Substrate -/// direction LR -/// subgraph Client -/// end -/// subgraph Runtime -/// end -/// end -/// ``` -/// -/// The client and the runtime of course need to communicate. This is done through two concepts: -/// -/// 1. Host functions: a way for the (Wasm) runtime to talk to the client. All host functions are -/// defined in [`sp-io`]. For example, [`sp-io::storage`] are the set of host functions that -/// allow the runtime to read and write data to the on-chain state. -/// 2. Runtime APIs: a way for the client to talk to the Wasm runtime. Runtime APIs are defined -/// using macros and utilities in [`sp-api`]. For example, [`sp-api::Core`] is the most basic -/// runtime API that any blockchain must implement in order to be able to (re) execute blocks. -/// -/// ```mermaid -/// graph TB -/// subgraph Substrate -/// direction LR -/// subgraph Client -/// end -/// subgraph Runtime -/// end -/// Client --runtime-api--> Runtime -/// Runtime --host-functions--> Client -/// end -/// ``` -/// -/// Finally, let's expand the diagram a bit further and look at the internals of each component: -/// -/// ```mermaid -/// graph TB -/// subgraph Substrate -/// direction LR -/// subgraph Client -/// Database -/// Networking -/// Consensus -/// end -/// subgraph Runtime -/// subgraph FRAME -/// direction LR -/// Governance -/// Currency -/// Staking -/// Identity -/// end -/// end -/// Client --runtime-api--> Runtime -/// Runtime --host-functions--> Client -/// end -/// ``` -/// -/// As noted the runtime contains all of the application specific logic of the blockchain. This is -/// usually written with `FRAME`. The client, on the other hand, contains reusable and generic -/// components that are not specific to one single blockchain, such as networking, database, and the -/// consensus engine. -/// -/// [`sp-io`]: ../../sp_io/index.html -/// [`sp-api`]: ../../sp_api/index.html -/// [`sp-io::storage`]: ../../sp_io/storage/index.html -/// [`sp-api::Core`]: ../../sp_api/trait.Core.html -pub mod substrate_diagram {} From 373b8ac78db5e0ca5bc6261874058c748fff0f8c Mon Sep 17 00:00:00 2001 From: Bulat Saifullin <bulat@parity.io> Date: Tue, 10 Oct 2023 16:40:57 +0200 Subject: [PATCH 24/54] Update testnet bootnode dns name (#1712) # Description Update the DNS name of bootnodes to unify the deployment. Each bootnode have 3 port exposed: `30333, 30334, 443`. Before, we had different DNS names for `30333, 30334` and `443` ports. It may confuse people and give the impression that it is two different nodes. Fixing it by using a single domain for all --- polkadot/node/service/chain-specs/rococo.json | 24 ++++++++++++------- .../node/service/chain-specs/westend.json | 22 +++++++++-------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/polkadot/node/service/chain-specs/rococo.json b/polkadot/node/service/chain-specs/rococo.json index 43dc959b57677..2648063641c91 100644 --- a/polkadot/node/service/chain-specs/rococo.json +++ b/polkadot/node/service/chain-specs/rococo.json @@ -3,14 +3,22 @@ "id": "rococo_v2_2", "chainType": "Live", "bootNodes": [ - "/dns/rococo-bootnode-0.parity-testnet.parity.io/tcp/30333/p2p/12D3KooWGikJMBmRiG5ofCqn8aijCijgfmZR5H9f53yUF3srm6Nm", - "/dns/rococo-bootnode-1.parity-testnet.parity.io/tcp/30333/p2p/12D3KooWLDfH9mHRCidrd5NfQjp7rRMUcJSEUwSvEKyu7xU2cG3d", - "/dns/rococo-bootnode-2.parity-testnet.parity.io/tcp/30333/p2p/12D3KooWSikgbrcWjVgSed7r1uXk4TeAieDnHKtrPDVZBu5XkQha", - "/dns/rococo-bootnode-3.parity-testnet.parity.io/tcp/30333/p2p/12D3KooWPeKuW1BBPv4pNr8xqEv7jqy7rQnS3oq9U7xTCvj9qt2k", - "/dns/rococo-bootnode-4.parity-testnet.parity.io/tcp/30333/p2p/12D3KooWNy7K8TNaP2Whcp3tsjBVUg2HcKMUvAArsimjvd1g31w4", - "/dns/rococo-bootnode-5.parity-testnet.parity.io/tcp/30333/p2p/12D3KooWAVV9DZfvJp2brvs5zcQDTBFxNmEFJKy2dsvezWL4Bmy8", - "/dns/rococo-bootnode-6.parity-testnet.parity.io/tcp/30333/p2p/12D3KooWM3hvXvaShyp7drQCavFHuwobkYdnCp2uHU5iRRAQwsw2", - "/dns/rococo-bootnode-7.parity-testnet.parity.io/tcp/30333/p2p/12D3KooWSbGtxfWCwn1tdmfZYESbmxzbTG2LKwKUrioDaZBcdMY4", + "/dns/rococo-bootnode-0.polkadot.io/tcp/30333/p2p/12D3KooWGikJMBmRiG5ofCqn8aijCijgfmZR5H9f53yUF3srm6Nm", + "/dns/rococo-bootnode-1.polkadot.io/tcp/30333/p2p/12D3KooWLDfH9mHRCidrd5NfQjp7rRMUcJSEUwSvEKyu7xU2cG3d", + "/dns/rococo-bootnode-2.polkadot.io/tcp/30333/p2p/12D3KooWSikgbrcWjVgSed7r1uXk4TeAieDnHKtrPDVZBu5XkQha", + "/dns/rococo-bootnode-3.polkadot.io/tcp/30333/p2p/12D3KooWPeKuW1BBPv4pNr8xqEv7jqy7rQnS3oq9U7xTCvj9qt2k", + "/dns/rococo-bootnode-4.polkadot.io/tcp/30333/p2p/12D3KooWNy7K8TNaP2Whcp3tsjBVUg2HcKMUvAArsimjvd1g31w4", + "/dns/rococo-bootnode-5.polkadot.io/tcp/30333/p2p/12D3KooWAVV9DZfvJp2brvs5zcQDTBFxNmEFJKy2dsvezWL4Bmy8", + "/dns/rococo-bootnode-6.polkadot.io/tcp/30333/p2p/12D3KooWM3hvXvaShyp7drQCavFHuwobkYdnCp2uHU5iRRAQwsw2", + "/dns/rococo-bootnode-7.polkadot.io/tcp/30333/p2p/12D3KooWSbGtxfWCwn1tdmfZYESbmxzbTG2LKwKUrioDaZBcdMY4", + "/dns/rococo-bootnode-0.polkadot.io/tcp/30334/ws/p2p/12D3KooWGikJMBmRiG5ofCqn8aijCijgfmZR5H9f53yUF3srm6Nm", + "/dns/rococo-bootnode-1.polkadot.io/tcp/30334/ws/p2p/12D3KooWLDfH9mHRCidrd5NfQjp7rRMUcJSEUwSvEKyu7xU2cG3d", + "/dns/rococo-bootnode-2.polkadot.io/tcp/30334/ws/p2p/12D3KooWSikgbrcWjVgSed7r1uXk4TeAieDnHKtrPDVZBu5XkQha", + "/dns/rococo-bootnode-3.polkadot.io/tcp/30334/ws/p2p/12D3KooWPeKuW1BBPv4pNr8xqEv7jqy7rQnS3oq9U7xTCvj9qt2k", + "/dns/rococo-bootnode-4.polkadot.io/tcp/30334/ws/p2p/12D3KooWNy7K8TNaP2Whcp3tsjBVUg2HcKMUvAArsimjvd1g31w4", + "/dns/rococo-bootnode-5.polkadot.io/tcp/30334/ws/p2p/12D3KooWAVV9DZfvJp2brvs5zcQDTBFxNmEFJKy2dsvezWL4Bmy8", + "/dns/rococo-bootnode-6.polkadot.io/tcp/30334/ws/p2p/12D3KooWM3hvXvaShyp7drQCavFHuwobkYdnCp2uHU5iRRAQwsw2", + "/dns/rococo-bootnode-7.polkadot.io/tcp/30334/ws/p2p/12D3KooWSbGtxfWCwn1tdmfZYESbmxzbTG2LKwKUrioDaZBcdMY4", "/dns/rococo-bootnode-0.polkadot.io/tcp/443/wss/p2p/12D3KooWGikJMBmRiG5ofCqn8aijCijgfmZR5H9f53yUF3srm6Nm", "/dns/rococo-bootnode-1.polkadot.io/tcp/443/wss/p2p/12D3KooWLDfH9mHRCidrd5NfQjp7rRMUcJSEUwSvEKyu7xU2cG3d", "/dns/rococo-bootnode-2.polkadot.io/tcp/443/wss/p2p/12D3KooWSikgbrcWjVgSed7r1uXk4TeAieDnHKtrPDVZBu5XkQha", diff --git a/polkadot/node/service/chain-specs/westend.json b/polkadot/node/service/chain-specs/westend.json index e57786f78a641..fd1f4550127fc 100644 --- a/polkadot/node/service/chain-specs/westend.json +++ b/polkadot/node/service/chain-specs/westend.json @@ -2,16 +2,18 @@ "name": "Westend", "id": "westend2", "bootNodes": [ - "/dns/0.westend.paritytech.net/tcp/30333/p2p/12D3KooWKer94o1REDPtAhjtYR4SdLehnSrN8PEhBnZm5NBoCrMC", - "/dns/0.westend.paritytech.net/tcp/30334/ws/p2p/12D3KooWKer94o1REDPtAhjtYR4SdLehnSrN8PEhBnZm5NBoCrMC", - "/dns/1.westend.paritytech.net/tcp/30333/p2p/12D3KooWPVPzs42GvRBShdUMtFsk4SvnByrSdWqb6aeAAHvLMSLS", - "/dns/1.westend.paritytech.net/tcp/30334/ws/p2p/12D3KooWPVPzs42GvRBShdUMtFsk4SvnByrSdWqb6aeAAHvLMSLS", - "/dns/2.westend.paritytech.net/tcp/30333/p2p/12D3KooWByVpK92hMi9CzTjyFg9cPHDU5ariTM3EPMq9vdh5S5Po", - "/dns/2.westend.paritytech.net/tcp/30334/ws/p2p/12D3KooWByVpK92hMi9CzTjyFg9cPHDU5ariTM3EPMq9vdh5S5Po", - "/dns/3.westend.paritytech.net/tcp/30333/p2p/12D3KooWGi1tCpKXLMYED9y28QXLnwgD4neYb1Arqq4QpeV1Sv3K", - "/dns/3.westend.paritytech.net/tcp/30334/ws/p2p/12D3KooWGi1tCpKXLMYED9y28QXLnwgD4neYb1Arqq4QpeV1Sv3K", - "/dns/westend-connect-0.polkadot.io/tcp/443/wss/p2p/12D3KooWNg8iUqhux7X7voNU9Nty5pzehrFJwkQwg1CJnqN3CTzE", - "/dns/westend-connect-1.polkadot.io/tcp/443/wss/p2p/12D3KooWAq2A7UNFS6725XFatD5QW7iYBezTLdAUx1SmRkxN79Ne", + "/dns/westend-bootnode-0.polkadot.io/tcp/30333/p2p/12D3KooWKer94o1REDPtAhjtYR4SdLehnSrN8PEhBnZm5NBoCrMC", + "/dns/westend-bootnode-0.polkadot.io/tcp/30334/ws/p2p/12D3KooWKer94o1REDPtAhjtYR4SdLehnSrN8PEhBnZm5NBoCrMC", + "/dns/westend-bootnode-0.polkadot.io/tcp/443/wss/p2p/12D3KooWKer94o1REDPtAhjtYR4SdLehnSrN8PEhBnZm5NBoCrMC", + "/dns/westend-bootnode-1.polkadot.io/tcp/30333/p2p/12D3KooWPVPzs42GvRBShdUMtFsk4SvnByrSdWqb6aeAAHvLMSLS", + "/dns/westend-bootnode-1.polkadot.io/tcp/30334/ws/p2p/12D3KooWPVPzs42GvRBShdUMtFsk4SvnByrSdWqb6aeAAHvLMSLS", + "/dns/westend-bootnode-1.polkadot.io/tcp/443/wss/p2p/12D3KooWPVPzs42GvRBShdUMtFsk4SvnByrSdWqb6aeAAHvLMSLS", + "/dns/westend-bootnode-2.polkadot.io/tcp/30333/p2p/12D3KooWByVpK92hMi9CzTjyFg9cPHDU5ariTM3EPMq9vdh5S5Po", + "/dns/westend-bootnode-2.polkadot.io/tcp/30334/ws/p2p/12D3KooWByVpK92hMi9CzTjyFg9cPHDU5ariTM3EPMq9vdh5S5Po", + "/dns/westend-bootnode-2.polkadot.io/tcp/443/wss/p2p/12D3KooWByVpK92hMi9CzTjyFg9cPHDU5ariTM3EPMq9vdh5S5Po", + "/dns/westend-bootnode-3.polkadot.io/tcp/30333/p2p/12D3KooWGi1tCpKXLMYED9y28QXLnwgD4neYb1Arqq4QpeV1Sv3K", + "/dns/westend-bootnode-3.polkadot.io/tcp/30334/ws/p2p/12D3KooWGi1tCpKXLMYED9y28QXLnwgD4neYb1Arqq4QpeV1Sv3K", + "/dns/westend-bootnode-3.polkadot.io/tcp/443/wss/p2p/12D3KooWGi1tCpKXLMYED9y28QXLnwgD4neYb1Arqq4QpeV1Sv3K", "/dns/boot.stake.plus/tcp/32333/p2p/12D3KooWK8fjVoSvMq5copQYMsdYreSGPGgcMbGMgbMDPfpf3sm7", "/dns/boot.stake.plus/tcp/32334/wss/p2p/12D3KooWK8fjVoSvMq5copQYMsdYreSGPGgcMbGMgbMDPfpf3sm7", "/dns/boot-node.helikon.io/tcp/7080/p2p/12D3KooWRFDPyT8vA8mLzh6dJoyujn4QNjeqi6Ch79eSMz9beKXC", From 55f354429c41e56a66c0b0142834280b77f31d4b Mon Sep 17 00:00:00 2001 From: Liam Aharon <liam.aharon@hotmail.com> Date: Wed, 11 Oct 2023 01:41:58 +1100 Subject: [PATCH 25/54] remote-ext: fix state download stall on slow connections and reduce memory usage (#1295) Original PR https://github.com/paritytech/substrate/pull/14746 --- ## Fixing stall ### Introduction I experienced an apparent stall downloading state from `https://rococo-try-runtime-node.parity-chains.parity.io:443` which was having networking difficulties only responding to my JSONRPC requests with 50-200KB/s of bandwidth. This PR fixes the issue causing the stall, and generally improves performance remote-ext when it downloads state by greatly reducing the chances of a timeout occuring. ### Description Introduces a new `REQUEST_DURATION_TARGET` constant and modifies `get_storage_data_dynamic_batch_size` to - Increase or decrease the batch size of the next request depending on whether the elapsed time of the last request was gt or lt the target - Reset the batch size to 1 if the request times out This fixes an issue on slow connections that can otherwise cause multiple timeouts and a stalled download when: 1. The batch size increases rapidly as remote-ext downloads keys with small associated storage values 2. remote-ext tries to process a large series of subsequent keys all with extremely large associated storage values (Rococo has a series of keys 1-5MB large) 3. The huge storage values download for 5 minutes until the request times out 4. The partially downloaded keys are thrown out and remote-ext tries again with a smaller batch size, but the batch size is still far too large and takes 5 minutes to be reduced again 5. The download will be essentially stalled for many hours while the above step cycles After this PR, the request size will - Not grow as large to begin with, as it is regulated downwards as the request duration exceeds the target - Drop immediately to 1 if the request times out. A timeout indicates the keys next in line to download have extremely large storage values compared to previously downloaded keys, and we need to reset the batch size to figure out what our new ideal batch size is. By not resetting down to 1, we risk the next request timing out again. ## Reducing memory As suggested by @bkchr, I adjusted `get_storage_data_dynamic_batch_size` from being recursive to a loop which allows removing a bunch of clones that were chewing through a lot of memory. I noticed actually it was using up to 50GB swap previously when downloading Polkadot keys on a slow connection, because it needed to recurse and clone a lot. After this change it uses only ~1.5GB memory. --- Cargo.lock | 12 -- .../frame/remote-externalities/Cargo.toml | 1 - .../frame/remote-externalities/src/lib.rs | 190 ++++++++++-------- 3 files changed, 102 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc8415a5a23c1..5481f917be4fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1096,17 +1096,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "async-recursion" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] - [[package]] name = "async-stream" version = "0.3.5" @@ -5286,7 +5275,6 @@ dependencies = [ name = "frame-remote-externalities" version = "0.10.0-dev" dependencies = [ - "async-recursion", "futures", "indicatif", "jsonrpsee", diff --git a/substrate/utils/frame/remote-externalities/Cargo.toml b/substrate/utils/frame/remote-externalities/Cargo.toml index ad6ab006da1dc..7067aed238aca 100644 --- a/substrate/utils/frame/remote-externalities/Cargo.toml +++ b/substrate/utils/frame/remote-externalities/Cargo.toml @@ -23,7 +23,6 @@ sp-runtime = { path = "../../../primitives/runtime" } tokio = { version = "1.22.0", features = ["macros", "rt-multi-thread"] } substrate-rpc-client = { path = "../rpc/client" } futures = "0.3" -async-recursion = "1.0.4" indicatif = "0.17.3" spinners = "4.1.0" tokio-retry = "0.3.0" diff --git a/substrate/utils/frame/remote-externalities/src/lib.rs b/substrate/utils/frame/remote-externalities/src/lib.rs index 072ea6ef5e597..71e9320ebeeb2 100644 --- a/substrate/utils/frame/remote-externalities/src/lib.rs +++ b/substrate/utils/frame/remote-externalities/src/lib.rs @@ -20,7 +20,6 @@ //! An equivalent of `sp_io::TestExternalities` that can load its state from a remote substrate //! based chain, or a local state snapshot file. -use async_recursion::async_recursion; use codec::{Compact, Decode, Encode}; use indicatif::{ProgressBar, ProgressStyle}; use jsonrpsee::{ @@ -44,7 +43,7 @@ use sp_runtime::{ use sp_state_machine::TestExternalities; use spinners::{Spinner, Spinners}; use std::{ - cmp::max, + cmp::{max, min}, fs, ops::{Deref, DerefMut}, path::{Path, PathBuf}, @@ -353,10 +352,11 @@ where const PARALLEL_REQUESTS: usize = 4; const BATCH_SIZE_INCREASE_FACTOR: f32 = 1.10; const BATCH_SIZE_DECREASE_FACTOR: f32 = 0.50; - const INITIAL_BATCH_SIZE: usize = 5000; + const REQUEST_DURATION_TARGET: Duration = Duration::from_secs(15); + const INITIAL_BATCH_SIZE: usize = 10; // nodes by default will not return more than 1000 keys per request const DEFAULT_KEY_DOWNLOAD_PAGE: u32 = 1000; - const KEYS_PAGE_MAX_RETRIES: usize = 12; + const MAX_RETRIES: usize = 12; const KEYS_PAGE_RETRY_INTERVAL: Duration = Duration::from_secs(5); async fn rpc_get_storage( @@ -411,8 +411,8 @@ where let keys = loop { // This loop can hit the node with very rapid requests, occasionally causing it to // error out in CI (https://github.com/paritytech/substrate/issues/14129), so we retry. - let retry_strategy = FixedInterval::new(Self::KEYS_PAGE_RETRY_INTERVAL) - .take(Self::KEYS_PAGE_MAX_RETRIES); + let retry_strategy = + FixedInterval::new(Self::KEYS_PAGE_RETRY_INTERVAL).take(Self::MAX_RETRIES); let get_page_closure = || self.get_keys_single_page(Some(prefix.clone()), last_key.clone(), at); let page = Retry::spawn(retry_strategy, get_page_closure).await?; @@ -448,8 +448,6 @@ where /// /// * `client` - An `Arc` wrapped `HttpClient` used for making the requests. /// * `payloads` - A vector of tuples containing a JSONRPC method name and `ArrayParams` - /// * `batch_size` - The initial batch size to use for the request. The batch size will be - /// adjusted dynamically in case of failure. /// /// # Returns /// @@ -485,80 +483,107 @@ where /// } /// } /// ``` - #[async_recursion] async fn get_storage_data_dynamic_batch_size( client: &HttpClient, payloads: Vec<(String, ArrayParams)>, - batch_size: usize, bar: &ProgressBar, ) -> Result<Vec<Option<StorageData>>, String> { - // All payloads have been processed - if payloads.is_empty() { - return Ok(vec![]) - }; - - log::debug!( - target: LOG_TARGET, - "Remaining payloads: {} Batch request size: {}", - payloads.len(), - batch_size, - ); + let mut all_data: Vec<Option<StorageData>> = vec![]; + let mut start_index = 0; + let mut retries = 0usize; + let mut batch_size = Self::INITIAL_BATCH_SIZE; + let total_payloads = payloads.len(); + + while start_index < total_payloads { + log::debug!( + target: LOG_TARGET, + "Remaining payloads: {} Batch request size: {}", + total_payloads - start_index, + batch_size, + ); - // Payloads to attempt to process this batch - let page = payloads.iter().take(batch_size).cloned().collect::<Vec<_>>(); + let end_index = usize::min(start_index + batch_size, total_payloads); + let page = &payloads[start_index..end_index]; - // Build the batch request - let mut batch = BatchRequestBuilder::new(); - for (method, params) in page.iter() { - batch - .insert(method, params.clone()) - .map_err(|_| "Invalid batch method and/or params")? - } - let batch_response = match client.batch_request::<Option<StorageData>>(batch).await { - Ok(batch_response) => batch_response, - Err(e) => { - if batch_size < 2 { - return Err(e.to_string()) - } + // Build the batch request + let mut batch = BatchRequestBuilder::new(); + for (method, params) in page.iter() { + batch + .insert(method, params.clone()) + .map_err(|_| "Invalid batch method and/or params")?; + } - log::debug!( - target: LOG_TARGET, - "Batch request failed, trying again with smaller batch size. {}", - e.to_string() - ); + let request_started = Instant::now(); + let batch_response = match client.batch_request::<Option<StorageData>>(batch).await { + Ok(batch_response) => { + retries = 0; + batch_response + }, + Err(e) => { + if retries > Self::MAX_RETRIES { + return Err(e.to_string()) + } + + retries += 1; + let failure_log = format!( + "Batch request failed ({}/{} retries). Error: {}", + retries, + Self::MAX_RETRIES, + e.to_string() + ); + // after 2 subsequent failures something very wrong is happening. log a warning + // and reset the batch size down to 1. + if retries >= 2 { + log::warn!("{}", failure_log); + batch_size = 1; + } else { + log::debug!("{}", failure_log); + // Decrease batch size by DECREASE_FACTOR + batch_size = + (batch_size as f32 * Self::BATCH_SIZE_DECREASE_FACTOR) as usize; + } + continue + }, + }; - return Self::get_storage_data_dynamic_batch_size( - client, - payloads, - max(1, (batch_size as f32 * Self::BATCH_SIZE_DECREASE_FACTOR) as usize), - bar, + let request_duration = request_started.elapsed(); + batch_size = if request_duration > Self::REQUEST_DURATION_TARGET { + // Decrease batch size + max(1, (batch_size as f32 * Self::BATCH_SIZE_DECREASE_FACTOR) as usize) + } else { + // Increase batch size, but not more than the remaining total payloads to process + min( + total_payloads - start_index, + max( + batch_size + 1, + (batch_size as f32 * Self::BATCH_SIZE_INCREASE_FACTOR) as usize, + ), ) - .await - }, - }; + }; + + log::debug!( + target: LOG_TARGET, + "Request duration: {:?} Target duration: {:?} Last batch size: {} Next batch size: {}", + request_duration, + Self::REQUEST_DURATION_TARGET, + end_index - start_index, + batch_size + ); - // Collect the data from this batch - let mut data: Vec<Option<StorageData>> = vec![]; - let batch_response_len = batch_response.len(); - for item in batch_response.into_iter() { - match item { - Ok(x) => data.push(x), - Err(e) => return Err(e.message().to_string()), + let batch_response_len = batch_response.len(); + for item in batch_response.into_iter() { + match item { + Ok(x) => all_data.push(x), + Err(e) => return Err(e.message().to_string()), + } } + bar.inc(batch_response_len as u64); + + // Update the start index for the next iteration + start_index = end_index; } - bar.inc(batch_response_len as u64); - // Return this data joined with the remaining keys - let remaining_payloads = payloads.iter().skip(batch_size).cloned().collect::<Vec<_>>(); - let mut rest = Self::get_storage_data_dynamic_batch_size( - client, - remaining_payloads, - max(batch_size + 1, (batch_size as f32 * Self::BATCH_SIZE_INCREASE_FACTOR) as usize), - bar, - ) - .await?; - data.append(&mut rest); - Ok(data) + Ok(all_data) } /// Synonym of `getPairs` that uses paged queries to first get the keys, and then @@ -605,12 +630,7 @@ where ); let payloads_chunked = payloads.chunks((&payloads.len() / Self::PARALLEL_REQUESTS).max(1)); let requests = payloads_chunked.map(|payload_chunk| { - Self::get_storage_data_dynamic_batch_size( - &client, - payload_chunk.to_vec(), - Self::INITIAL_BATCH_SIZE, - &bar, - ) + Self::get_storage_data_dynamic_batch_size(&client, payload_chunk.to_vec(), &bar) }); // Execute the requests and move the Result outside. let storage_data_result: Result<Vec<_>, _> = @@ -683,20 +703,14 @@ where .collect::<Vec<_>>(); let bar = ProgressBar::new(payloads.len() as u64); - let storage_data = match Self::get_storage_data_dynamic_batch_size( - client, - payloads, - Self::INITIAL_BATCH_SIZE, - &bar, - ) - .await - { - Ok(storage_data) => storage_data, - Err(e) => { - log::error!(target: LOG_TARGET, "batch processing failed: {:?}", e); - return Err("batch processing failed") - }, - }; + let storage_data = + match Self::get_storage_data_dynamic_batch_size(client, payloads, &bar).await { + Ok(storage_data) => storage_data, + Err(e) => { + log::error!(target: LOG_TARGET, "batch processing failed: {:?}", e); + return Err("batch processing failed") + }, + }; assert_eq!(child_keys_len, storage_data.len()); From 3f5edc52b22f39516c3e1d3bef0023bd6a48daf6 Mon Sep 17 00:00:00 2001 From: Keith Yeung <kungfukeith11@gmail.com> Date: Tue, 10 Oct 2023 17:32:11 +0200 Subject: [PATCH 26/54] Use safe math when pruning statuses (#1835) Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com> --- cumulus/pallets/xcmp-queue/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cumulus/pallets/xcmp-queue/src/lib.rs b/cumulus/pallets/xcmp-queue/src/lib.rs index 1cb92f5951869..7ee07a7beb0a7 100644 --- a/cumulus/pallets/xcmp-queue/src/lib.rs +++ b/cumulus/pallets/xcmp-queue/src/lib.rs @@ -1129,7 +1129,7 @@ impl<T: Config> XcmpMessageSource for Pallet<T> { let pruned = old_statuses_len - statuses.len(); // removing an item from status implies a message being sent, so the result messages must // be no less than the pruned channels. - statuses.rotate_left(result.len() - pruned); + statuses.rotate_left(result.len().saturating_sub(pruned)); <OutboundXcmpStatus<T>>::put(statuses); From 5adcb3e10638c8abcfd532b9163990d584fc5832 Mon Sep 17 00:00:00 2001 From: Sam Johnson <sam@durosoft.com> Date: Tue, 10 Oct 2023 13:48:50 -0400 Subject: [PATCH 27/54] upgrade to macro_magic 0.4.3 (#1832) # Description Upgrades `macro_magic` to 0.4.3, which introduces the ability to have `export_tokens` use the same name as the underlying item for its auto-generated macro name. Ultimately this will allow for better dev ux in our derive_impl feature. --- Cargo.lock | 16 ++++++++-------- substrate/frame/support/Cargo.toml | 2 +- substrate/frame/support/procedural/Cargo.toml | 2 +- substrate/frame/support/procedural/src/lib.rs | 9 +++++++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5481f917be4fd..d8e7d055d7700 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7616,9 +7616,9 @@ dependencies = [ [[package]] name = "macro_magic" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aee866bfee30d2d7e83835a4574aad5b45adba4cc807f2a3bbba974e5d4383c9" +checksum = "e03844fc635e92f3a0067e25fa4bf3e3dbf3f2927bf3aa01bb7bc8f1c428949d" dependencies = [ "macro_magic_core", "macro_magic_macros", @@ -7628,9 +7628,9 @@ dependencies = [ [[package]] name = "macro_magic_core" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e766a20fd9c72bab3e1e64ed63f36bd08410e75803813df210d1ce297d7ad00" +checksum = "468155613a44cfd825f1fb0ffa532b018253920d404e6fca1e8d43155198a46d" dependencies = [ "const-random", "derive-syn-parse", @@ -7642,9 +7642,9 @@ dependencies = [ [[package]] name = "macro_magic_core_macros" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c12469fc165526520dff2807c2975310ab47cf7190a45b99b49a7dc8befab17b" +checksum = "9ea73aa640dc01d62a590d48c0c3521ed739d53b27f919b25c3551e233481654" dependencies = [ "proc-macro2", "quote", @@ -7653,9 +7653,9 @@ dependencies = [ [[package]] name = "macro_magic_macros" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fb85ec1620619edf2984a7693497d4ec88a9665d8b87e942856884c92dbf2a" +checksum = "ef9d79ae96aaba821963320eb2b6e34d17df1e5a83d8a1985c29cc5be59577b3" dependencies = [ "macro_magic_core", "quote", diff --git a/substrate/frame/support/Cargo.toml b/substrate/frame/support/Cargo.toml index 5cb5d6d12ab78..65f4885b15999 100644 --- a/substrate/frame/support/Cargo.toml +++ b/substrate/frame/support/Cargo.toml @@ -30,7 +30,7 @@ sp-weights = { path = "../../primitives/weights", default-features = false} sp-debug-derive = { path = "../../primitives/debug-derive", default-features = false} sp-metadata-ir = { path = "../../primitives/metadata-ir", default-features = false} tt-call = "1.0.8" -macro_magic = "0.4.2" +macro_magic = "0.5.0" frame-support-procedural = { path = "procedural", default-features = false} paste = "1.0" sp-state-machine = { path = "../../primitives/state-machine", default-features = false, optional = true} diff --git a/substrate/frame/support/procedural/Cargo.toml b/substrate/frame/support/procedural/Cargo.toml index 9781a88251428..45ed1750a5287 100644 --- a/substrate/frame/support/procedural/Cargo.toml +++ b/substrate/frame/support/procedural/Cargo.toml @@ -23,8 +23,8 @@ proc-macro2 = "1.0.56" quote = "1.0.28" syn = { version = "2.0.38", features = ["full"] } frame-support-procedural-tools = { path = "tools" } +macro_magic = { version = "0.5.0", features = ["proc_support"] } proc-macro-warning = { version = "1.0.0", default-features = false } -macro_magic = { version = "0.4.2", features = ["proc_support"] } expander = "2.0.0" sp-core-hashing = { path = "../../../primitives/core/hashing" } diff --git a/substrate/frame/support/procedural/src/lib.rs b/substrate/frame/support/procedural/src/lib.rs index da4cb41fe4f2f..b4f6acb07cc81 100644 --- a/substrate/frame/support/procedural/src/lib.rs +++ b/substrate/frame/support/procedural/src/lib.rs @@ -864,7 +864,12 @@ pub fn register_default_impl(attrs: TokenStream, tokens: TokenStream) -> TokenSt let item_impl = syn::parse_macro_input!(tokens as ItemImpl); // internally wrap macro_magic's `#[export_tokens]` macro - match macro_magic::mm_core::export_tokens_internal(attrs, item_impl.to_token_stream(), true) { + match macro_magic::mm_core::export_tokens_internal( + attrs, + item_impl.to_token_stream(), + true, + true, + ) { Ok(tokens) => tokens.into(), Err(err) => err.to_compile_error().into(), } @@ -1565,7 +1570,7 @@ pub fn pallet_section(attr: TokenStream, tokens: TokenStream) -> TokenStream { let _mod = parse_macro_input!(tokens_clone as ItemMod); // use macro_magic's export_tokens as the internal implementation otherwise - match macro_magic::mm_core::export_tokens_internal(attr, tokens, false) { + match macro_magic::mm_core::export_tokens_internal(attr, tokens, false, true) { Ok(tokens) => tokens.into(), Err(err) => err.to_compile_error().into(), } From 294e99831dbcc52d8d167e16551b214a5bb536c2 Mon Sep 17 00:00:00 2001 From: gupnik <17176722+gupnik@users.noreply.github.com> Date: Wed, 11 Oct 2023 07:26:13 +0200 Subject: [PATCH 28/54] Fixes path issue in derive-impl (#1823) Needs https://github.com/sam0x17/macro_magic/pull/13 The associated PR allows the export of tokens from macro_magic at the specified path. This fixes the path issue in derive-impl. Now, we can import the default config using the standard rust syntax: ```rust use frame_system::config_preludes::TestDefaultConfig; [derive_impl(TestDefaultConfig as frame_system::DefaultConfig)] impl frame_system::DefaultConfig for Test { //.... } ``` --- substrate/frame/examples/default-config/src/lib.rs | 7 ++++--- substrate/frame/support/procedural/src/lib.rs | 6 +++--- .../tests/derive_impl_ui/bad_default_impl_path.stderr | 8 +++----- ...nject_runtime_type_fails_when_type_not_in_scope.stderr | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/substrate/frame/examples/default-config/src/lib.rs b/substrate/frame/examples/default-config/src/lib.rs index d2eade0ccff1e..8a1f6f9d6a82c 100644 --- a/substrate/frame/examples/default-config/src/lib.rs +++ b/substrate/frame/examples/default-config/src/lib.rs @@ -26,7 +26,7 @@ //! Study the following types: //! //! - [`pallet::DefaultConfig`], and how it differs from [`pallet::Config`]. -//! - [`pallet::config_preludes::TestDefaultConfig`] and how it implements +//! - [`struct@pallet::config_preludes::TestDefaultConfig`] and how it implements //! [`pallet::DefaultConfig`]. //! - Notice how [`pallet::DefaultConfig`] is independent of [`frame_system::Config`]. @@ -83,11 +83,12 @@ pub mod pallet { // This will help use not need to disambiguate anything when using `derive_impl`. use super::*; use frame_support::derive_impl; + use frame_system::config_preludes::TestDefaultConfig as SystemTestDefaultConfig; /// A type providing default configurations for this pallet in testing environment. pub struct TestDefaultConfig; - #[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig, no_aggregated_types)] + #[derive_impl(SystemTestDefaultConfig as frame_system::DefaultConfig, no_aggregated_types)] impl frame_system::DefaultConfig for TestDefaultConfig {} #[frame_support::register_default_impl(TestDefaultConfig)] @@ -109,7 +110,7 @@ pub mod pallet { /// example, we simple derive `frame_system::config_preludes::TestDefaultConfig` again. pub struct OtherDefaultConfig; - #[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig, no_aggregated_types)] + #[derive_impl(SystemTestDefaultConfig as frame_system::DefaultConfig, no_aggregated_types)] impl frame_system::DefaultConfig for OtherDefaultConfig {} #[frame_support::register_default_impl(OtherDefaultConfig)] diff --git a/substrate/frame/support/procedural/src/lib.rs b/substrate/frame/support/procedural/src/lib.rs index b4f6acb07cc81..07b5a50da417b 100644 --- a/substrate/frame/support/procedural/src/lib.rs +++ b/substrate/frame/support/procedural/src/lib.rs @@ -34,7 +34,7 @@ mod transactional; mod tt_macro; use frame_support_procedural_tools::generate_crate_access_2018; -use macro_magic::import_tokens_attr; +use macro_magic::{import_tokens_attr, import_tokens_attr_verbatim}; use proc_macro::TokenStream; use quote::{quote, ToTokens}; use std::{cell::RefCell, str::FromStr}; @@ -751,7 +751,7 @@ pub fn storage_alias(attributes: TokenStream, input: TokenStream) -> TokenStream /// Items that lack a `syn::Ident` for whatever reason are first checked to see if they exist, /// verbatim, in the local/destination trait before they are copied over, so you should not need to /// worry about collisions between identical unnamed items. -#[import_tokens_attr { +#[import_tokens_attr_verbatim { format!( "{}::macro_magic", match generate_crate_access_2018("frame-support") { @@ -868,7 +868,7 @@ pub fn register_default_impl(attrs: TokenStream, tokens: TokenStream) -> TokenSt attrs, item_impl.to_token_stream(), true, - true, + false, ) { Ok(tokens) => tokens.into(), Err(err) => err.to_compile_error().into(), diff --git a/substrate/frame/support/test/tests/derive_impl_ui/bad_default_impl_path.stderr b/substrate/frame/support/test/tests/derive_impl_ui/bad_default_impl_path.stderr index 5cfbd8c886249..c91226ea9c310 100644 --- a/substrate/frame/support/test/tests/derive_impl_ui/bad_default_impl_path.stderr +++ b/substrate/frame/support/test/tests/derive_impl_ui/bad_default_impl_path.stderr @@ -1,7 +1,5 @@ -error: cannot find macro `__export_tokens_tt_tiger` in this scope - --> tests/derive_impl_ui/bad_default_impl_path.rs:59:1 +error: cannot find macro `Tiger` in this scope + --> tests/derive_impl_ui/bad_default_impl_path.rs:59:15 | 59 | #[derive_impl(Tiger as Animal)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `frame_support::macro_magic::forward_tokens` (in Nightly builds, run with -Z macro-backtrace for more info) + | ^^^^^ diff --git a/substrate/frame/support/test/tests/derive_impl_ui/inject_runtime_type_fails_when_type_not_in_scope.stderr b/substrate/frame/support/test/tests/derive_impl_ui/inject_runtime_type_fails_when_type_not_in_scope.stderr index 79b50a940b802..f3ac6b2328110 100644 --- a/substrate/frame/support/test/tests/derive_impl_ui/inject_runtime_type_fails_when_type_not_in_scope.stderr +++ b/substrate/frame/support/test/tests/derive_impl_ui/inject_runtime_type_fails_when_type_not_in_scope.stderr @@ -7,4 +7,4 @@ error[E0412]: cannot find type `RuntimeCall` in this scope 35 | #[derive_impl(Pallet)] // Injects type RuntimeCall = RuntimeCall; | ---------------------- in this macro invocation | - = note: this error originates in the macro `__export_tokens_tt_pallet` which comes from the expansion of the macro `frame_support::macro_magic::forward_tokens` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `Pallet` which comes from the expansion of the macro `frame_support::macro_magic::forward_tokens_verbatim` (in Nightly builds, run with -Z macro-backtrace for more info) From cfb29254f74412cea35e8048d8aea94bc789fcb1 Mon Sep 17 00:00:00 2001 From: Branislav Kontur <bkontur@gmail.com> Date: Wed, 11 Oct 2023 10:32:49 +0200 Subject: [PATCH 29/54] Xcm emulator nits (#1649) # Desription ## Summary This PR introduces several nits and tweaks to xcm emulator tests for system parachains. ## Explanation **Deduplicate `XcmPallet::send(` with root origin code** - Introduced `send_transact_to_parachain` which could be easily reuse for scenarios like _governance call from relay chain to parachain_. **Refactor `send_transact_sudo_from_relay_to_system_para_works`** - Test covered just one use-case which was moved to the `do_force_create_asset_from_relay_to_system_para`, so now we can extend this test with more _governance-like_ senarios. - Renamed to `send_transact_as_superuser_from_relay_to_system_para_works`. **Remove `send_transact_native_from_relay_to_system_para_fails` test** - This test and/or description is kind of misleading, because system paras support Native from relay chain by `RelayChainAsNative` with correct xcm origin. - It tested only sending on relay chain which should go directly to the relay chain unit-tests (does not even need to be in xcm emulator level). ## Future directions Check restructure parachains integration tests [issue](https://github.com/paritytech/polkadot-sdk/issues/1389) and [PR with more TODOs](https://github.com/paritytech/polkadot-sdk/pull/1693). --------- Co-authored-by: Ignacio Palacios <ignacio.palacios.santos@gmail.com> --- .../src/tests/reserve_transfer.rs | 2 + .../asset-hub-westend/src/tests/send.rs | 51 ++-------- .../src/tests/set_xcm_versions.rs | 38 +++---- .../emulated/common/src/impls.rs | 98 ++++++++++++------- .../emulated/common/src/lib.rs | 14 +-- 5 files changed, 89 insertions(+), 114 deletions(-) diff --git a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/reserve_transfer.rs b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/reserve_transfer.rs index 805c8811b2ddf..8f8b7a7dde777 100644 --- a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/reserve_transfer.rs +++ b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/reserve_transfer.rs @@ -349,6 +349,7 @@ fn limited_reserve_transfer_asset_from_system_para_to_para() { ASSET_MIN_BALANCE, true, AssetHubWestendSender::get(), + Some(Weight::from_parts(1_019_445_000, 200_000)), ASSET_MIN_BALANCE * 1000000, ); @@ -384,6 +385,7 @@ fn reserve_transfer_asset_from_system_para_to_para() { ASSET_MIN_BALANCE, true, AssetHubWestendSender::get(), + Some(Weight::from_parts(1_019_445_000, 200_000)), ASSET_MIN_BALANCE * 1000000, ); diff --git a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/send.rs b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/send.rs index d0a71e88c674a..e603af685bb5c 100644 --- a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/send.rs +++ b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/send.rs @@ -16,52 +16,16 @@ use crate::*; /// Relay Chain should be able to execute `Transact` instructions in System Parachain -/// when `OriginKind::Superuser` and signer is `sudo` +/// when `OriginKind::Superuser`. #[test] -fn send_transact_sudo_from_relay_to_system_para_works() { - // Init tests variables - let root_origin = <Westend as Chain>::RuntimeOrigin::root(); - let system_para_destination = Westend::child_location_of(AssetHubWestend::para_id()).into(); - let asset_owner: AccountId = AssetHubWestendSender::get().into(); - let xcm = AssetHubWestend::force_create_asset_xcm( - OriginKind::Superuser, +fn send_transact_as_superuser_from_relay_to_system_para_works() { + AssetHubWestend::force_create_asset_from_relay_as_root( ASSET_ID, - asset_owner.clone(), + ASSET_MIN_BALANCE, true, - 1000, - ); - // Send XCM message from Relay Chain - Westend::execute_with(|| { - assert_ok!(<Westend as WestendPallet>::XcmPallet::send( - root_origin, - bx!(system_para_destination), - bx!(xcm), - )); - - Westend::assert_xcm_pallet_sent(); - }); - - // Receive XCM message in Assets Parachain - AssetHubWestend::execute_with(|| { - type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent; - - AssetHubWestend::assert_dmp_queue_complete(Some(Weight::from_parts( - 1_019_445_000, - 200_000, - ))); - - assert_expected_events!( - AssetHubWestend, - vec![ - RuntimeEvent::Assets(pallet_assets::Event::ForceCreated { asset_id, owner }) => { - asset_id: *asset_id == ASSET_ID, - owner: *owner == asset_owner, - }, - ] - ); - - assert!(<AssetHubWestend as AssetHubWestendPallet>::Assets::asset_exists(ASSET_ID)); - }); + AssetHubWestendSender::get().into(), + Some(Weight::from_parts(1_019_445_000, 200_000)), + ) } /// Parachain should be able to send XCM paying its fee with sufficient asset @@ -78,6 +42,7 @@ fn send_xcm_from_para_to_system_para_paying_fee_with_assets_works() { ASSET_MIN_BALANCE, true, para_sovereign_account.clone(), + Some(Weight::from_parts(1_019_445_000, 200_000)), ASSET_MIN_BALANCE * 1000000000, ); diff --git a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/set_xcm_versions.rs b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/set_xcm_versions.rs index 2720095aac00d..2133d5e5fb7c7 100644 --- a/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/set_xcm_versions.rs +++ b/cumulus/parachains/integration-tests/emulated/assets/asset-hub-westend/src/tests/set_xcm_versions.rs @@ -47,32 +47,22 @@ fn relay_sets_system_para_xcm_supported_version() { #[test] fn system_para_sets_relay_xcm_supported_version() { // Init test variables - let sudo_origin = <Westend as Chain>::RuntimeOrigin::root(); let parent_location = AssetHubWestend::parent_location(); - let system_para_destination: VersionedMultiLocation = - Westend::child_location_of(AssetHubWestend::para_id()).into(); - let call = <AssetHubWestend as Chain>::RuntimeCall::PolkadotXcm(pallet_xcm::Call::< - <AssetHubWestend as Chain>::Runtime, - >::force_xcm_version { - location: bx!(parent_location), - version: XCM_V3, - }) - .encode() - .into(); - let origin_kind = OriginKind::Superuser; - - let xcm = xcm_transact_unpaid_execution(call, origin_kind); - - // System Parachain sets supported version for Relay Chain throught it - Westend::execute_with(|| { - assert_ok!(<Westend as WestendPallet>::XcmPallet::send( - sudo_origin, - bx!(system_para_destination), - bx!(xcm), - )); + let force_xcm_version_call = + <AssetHubWestend as Chain>::RuntimeCall::PolkadotXcm(pallet_xcm::Call::< + <AssetHubWestend as Chain>::Runtime, + >::force_xcm_version { + location: bx!(parent_location), + version: XCM_V3, + }) + .encode() + .into(); - Westend::assert_xcm_pallet_sent(); - }); + // System Parachain sets supported version for Relay Chain through it + Westend::send_unpaid_transact_to_parachain_as_root( + AssetHubWestend::para_id(), + force_xcm_version_call, + ); // System Parachain receive the XCM message AssetHubWestend::execute_with(|| { diff --git a/cumulus/parachains/integration-tests/emulated/common/src/impls.rs b/cumulus/parachains/integration-tests/emulated/common/src/impls.rs index 5e11922d859c6..bb4c9d102e98a 100644 --- a/cumulus/parachains/integration-tests/emulated/common/src/impls.rs +++ b/cumulus/parachains/integration-tests/emulated/common/src/impls.rs @@ -54,7 +54,7 @@ pub use polkadot_runtime_parachains::{ inclusion::{AggregateMessageOrigin, UmpQueueId}, }; pub use xcm::{ - prelude::{OriginKind, Outcome, VersionedXcm, Weight}, + prelude::{MultiLocation, OriginKind, Outcome, VersionedXcm, Weight}, v3::Error, DoubleEncoded, }; @@ -80,21 +80,11 @@ impl From<u32> for LaneIdWrapper { type BridgeHubRococoRuntime = <BridgeHubRococo as Chain>::Runtime; type BridgeHubWococoRuntime = <BridgeHubWococo as Chain>::Runtime; -// TODO: uncomment when https://github.com/paritytech/polkadot-sdk/pull/1352 is merged -// type BridgeHubPolkadotRuntime = <BridgeHubPolkadot as Chain>::Runtime; -// type BridgeHubKusamaRuntime = <BridgeHubKusama as Chain>::Runtime; - pub type RococoWococoMessageHandler = BridgeHubMessageHandler<BridgeHubRococoRuntime, BridgeHubWococoRuntime, Instance2>; pub type WococoRococoMessageHandler = BridgeHubMessageHandler<BridgeHubWococoRuntime, BridgeHubRococoRuntime, Instance2>; -// TODO: uncomment when https://github.com/paritytech/polkadot-sdk/pull/1352 is merged -// pub type PolkadotKusamaMessageHandler -// = BridgeHubMessageHandler<BridgeHubPolkadotRuntime, BridgeHubKusamaRuntime, Instance1>; -// pub type KusamaPolkadotMessageHandler -// = BridgeHubMessageHandler<BridgeHubKusamaRuntime, BridgeHubPolkadoRuntime, Instance1>; - impl<S, T, I> BridgeMessageHandler for BridgeHubMessageHandler<S, T, I> where S: Config<Instance1>, @@ -356,6 +346,37 @@ macro_rules! impl_hrmp_channels_helpers_for_relay_chain { }; } +#[macro_export] +macro_rules! impl_send_transact_helpers_for_relay_chain { + ( $chain:ident ) => { + $crate::impls::paste::paste! { + impl $chain { + /// A root origin (as governance) sends `xcm::Transact` with `UnpaidExecution` and encoded `call` to child parachain. + pub fn send_unpaid_transact_to_parachain_as_root( + recipient: $crate::impls::ParaId, + call: $crate::impls::DoubleEncoded<()> + ) { + use $crate::impls::{bx, Chain, RelayChain}; + + <Self as $crate::impls::TestExt>::execute_with(|| { + let root_origin = <Self as Chain>::RuntimeOrigin::root(); + let destination: $crate::impls::MultiLocation = <Self as RelayChain>::child_location_of(recipient); + let xcm = $crate::impls::xcm_transact_unpaid_execution(call, $crate::impls::OriginKind::Superuser); + + // Send XCM `Transact` + $crate::impls::assert_ok!(<Self as [<$chain Pallet>]>::XcmPallet::send( + root_origin, + bx!(destination.into()), + bx!(xcm), + )); + Self::assert_xcm_pallet_sent(); + }); + } + } + } + }; +} + #[macro_export] macro_rules! impl_accounts_helpers_for_parachain { ( $chain:ident ) => { @@ -616,53 +637,58 @@ macro_rules! impl_assets_helpers_for_parachain { min_balance: u128, is_sufficient: bool, asset_owner: $crate::impls::AccountId, + dmp_weight_threshold: Option<$crate::impls::Weight>, amount_to_mint: u128, ) { - use $crate::impls::{bx, Chain, RelayChain, Parachain, Inspect, TestExt}; - // Init values for Relay Chain - let root_origin = <$relay_chain as Chain>::RuntimeOrigin::root(); - let destination = <$relay_chain>::child_location_of(<$chain>::para_id()); - let xcm = Self::force_create_asset_xcm( - $crate::impls::OriginKind::Superuser, + use $crate::impls::Chain; + + // Force create asset + Self::force_create_asset_from_relay_as_root( id, - asset_owner.clone(), - is_sufficient, min_balance, + is_sufficient, + asset_owner.clone(), + dmp_weight_threshold ); - <$relay_chain>::execute_with(|| { - $crate::impls::assert_ok!(<$relay_chain as [<$relay_chain Pallet>]>::XcmPallet::send( - root_origin, - bx!(destination.into()), - bx!(xcm), - )); + // Mint asset for System Parachain's sender + let signed_origin = <Self as Chain>::RuntimeOrigin::signed(asset_owner.clone()); + Self::mint_asset(signed_origin, id, asset_owner, amount_to_mint); + } - <$relay_chain>::assert_xcm_pallet_sent(); - }); + /// Relay Chain sends `Transact` instruction with `force_create_asset` to Parachain with `Assets` instance of `pallet_assets` . + pub fn force_create_asset_from_relay_as_root( + id: u32, + min_balance: u128, + is_sufficient: bool, + asset_owner: $crate::impls::AccountId, + dmp_weight_threshold: Option<$crate::impls::Weight>, + ) { + use $crate::impls::{Parachain, Inspect, TestExt}; - Self::execute_with(|| { - Self::assert_dmp_queue_complete(Some($crate::impls::Weight::from_parts(1_019_445_000, 200_000))); + <$relay_chain>::send_unpaid_transact_to_parachain_as_root( + Self::para_id(), + Self::force_create_asset_call(id, asset_owner.clone(), is_sufficient, min_balance), + ); + // Receive XCM message in Assets Parachain + Self::execute_with(|| { type RuntimeEvent = <$chain as $crate::impls::Chain>::RuntimeEvent; + Self::assert_dmp_queue_complete(dmp_weight_threshold); + $crate::impls::assert_expected_events!( Self, vec![ - // Asset has been created RuntimeEvent::Assets($crate::impls::pallet_assets::Event::ForceCreated { asset_id, owner }) => { asset_id: *asset_id == id, - owner: *owner == asset_owner.clone(), + owner: *owner == asset_owner, }, ] ); assert!(<Self as [<$chain Pallet>]>::Assets::asset_exists(id.into())); }); - - let signed_origin = <Self as Chain>::RuntimeOrigin::signed(asset_owner.clone()); - - // Mint asset for System Parachain's sender - Self::mint_asset(signed_origin, id, asset_owner, amount_to_mint); } } } diff --git a/cumulus/parachains/integration-tests/emulated/common/src/lib.rs b/cumulus/parachains/integration-tests/emulated/common/src/lib.rs index 068f0238f4978..c2e065ccadcdf 100644 --- a/cumulus/parachains/integration-tests/emulated/common/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/common/src/lib.rs @@ -250,30 +250,22 @@ decl_test_bridges! { target = BridgeHubRococo, handler = WococoRococoMessageHandler } - // TODO: uncomment when https://github.com/paritytech/polkadot-sdk/pull/1352 is merged - // pub struct PolkadotKusamaMockBridge { - // source = BridgeHubPolkadot, - // target = BridgeHubKusama, - // handler = PolkadotKusamaMessageHandler - // }, - // pub struct KusamaPolkadotMockBridge { - // source = BridgeHubKusama, - // target = BridgeHubPolkadot, - // handler = KusamaPolkadotMessageHandler - // } } // Westend implementation impl_accounts_helpers_for_relay_chain!(Westend); impl_assert_events_helpers_for_relay_chain!(Westend); +impl_send_transact_helpers_for_relay_chain!(Westend); // Rococo implementation impl_accounts_helpers_for_relay_chain!(Rococo); impl_assert_events_helpers_for_relay_chain!(Rococo); +impl_send_transact_helpers_for_relay_chain!(Rococo); // Wococo implementation impl_accounts_helpers_for_relay_chain!(Wococo); impl_assert_events_helpers_for_relay_chain!(Wococo); +impl_send_transact_helpers_for_relay_chain!(Wococo); // AssetHubWestend implementation impl_accounts_helpers_for_parachain!(AssetHubWestend); From 132ba0c89fc1d48d770f28a5d5448c9dd1bb164a Mon Sep 17 00:00:00 2001 From: Marcin S <marcin@realemail.net> Date: Wed, 11 Oct 2023 16:13:07 +0200 Subject: [PATCH 30/54] PVF worker: bump landlock, update ABI docs (#1850) --- Cargo.lock | 4 +- polkadot/node/core/pvf/common/Cargo.toml | 2 +- .../core/pvf/common/src/worker/security.rs | 52 +++++++++++++++++-- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8e7d055d7700..58bacc9db7399 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6874,9 +6874,9 @@ dependencies = [ [[package]] name = "landlock" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520baa32708c4e957d2fc3a186bc5bd8d26637c33137f399ddfc202adb240068" +checksum = "1530c5b973eeed4ac216af7e24baf5737645a6272e361f1fb95710678b67d9cc" dependencies = [ "enumflags2", "libc", diff --git a/polkadot/node/core/pvf/common/Cargo.toml b/polkadot/node/core/pvf/common/Cargo.toml index 0f7308396d809..4bdacca72f420 100644 --- a/polkadot/node/core/pvf/common/Cargo.toml +++ b/polkadot/node/core/pvf/common/Cargo.toml @@ -29,7 +29,7 @@ sp-io = { path = "../../../../../substrate/primitives/io" } sp-tracing = { path = "../../../../../substrate/primitives/tracing" } [target.'cfg(target_os = "linux")'.dependencies] -landlock = "0.2.0" +landlock = "0.3.0" [dev-dependencies] assert_matches = "1.4.0" diff --git a/polkadot/node/core/pvf/common/src/worker/security.rs b/polkadot/node/core/pvf/common/src/worker/security.rs index b7abf028f9410..1b76141774485 100644 --- a/polkadot/node/core/pvf/common/src/worker/security.rs +++ b/polkadot/node/core/pvf/common/src/worker/security.rs @@ -223,13 +223,22 @@ pub mod landlock { /// Landlock ABI version. We use ABI V1 because: /// /// 1. It is supported by our reference kernel version. - /// 2. Later versions do not (yet) provide additional security. + /// 2. Later versions do not (yet) provide additional security that would benefit us. /// - /// # Versions (as of June 2023) + /// # Versions (as of October 2023) /// /// - Polkadot reference kernel version: 5.16+ - /// - ABI V1: 5.13 - introduces landlock, including full restrictions on file reads - /// - ABI V2: 5.19 - adds ability to configure file renaming (not used by us) + /// + /// - ABI V1: kernel 5.13 - Introduces landlock, including full restrictions on file reads. + /// + /// - ABI V2: kernel 5.19 - Adds ability to prevent file renaming. Does not help us. During + /// execution an attacker can only affect the name of a symlinked artifact and not the + /// original one. + /// + /// - ABI V3: kernel 6.2 - Adds ability to prevent file truncation. During execution, can + /// prevent attackers from affecting a symlinked artifact. We don't strictly need this as we + /// plan to check for file integrity anyway; see + /// <https://github.com/paritytech/polkadot-sdk/issues/677>. /// /// # Determinism /// @@ -335,7 +344,7 @@ pub mod landlock { A: Into<BitFlags<AccessFs>>, { let mut ruleset = - Ruleset::new().handle_access(AccessFs::from_all(LANDLOCK_ABI))?.create()?; + Ruleset::default().handle_access(AccessFs::from_all(LANDLOCK_ABI))?.create()?; for (fs_path, access_bits) in fs_exceptions { let paths = &[fs_path.as_ref().to_owned()]; let mut rules = path_beneath_rules(paths, access_bits).peekable(); @@ -466,5 +475,38 @@ pub mod landlock { assert!(handle.join().is_ok()); } + + // Test that checks whether landlock under our ABI version is able to truncate files. + #[test] + fn restricted_thread_can_truncate_file() { + // TODO: This would be nice: <https://github.com/rust-lang/rust/issues/68007>. + if !check_is_fully_enabled() { + return + } + + // Restricted thread can truncate file. + let handle = + thread::spawn(|| { + // Create and write a file. This should succeed before any landlock + // restrictions are applied. + const TEXT: &str = "foo"; + let tmpfile = tempfile::NamedTempFile::new().unwrap(); + let path = tmpfile.path(); + + fs::write(path, TEXT).unwrap(); + + // Apply Landlock with all exceptions under the current ABI. + let status = try_restrict(vec![(path, AccessFs::from_all(LANDLOCK_ABI))]); + if !matches!(status, Ok(RulesetStatus::FullyEnforced)) { + panic!("Ruleset should be enforced since we checked if landlock is enabled: {:?}", status); + } + + // Try to truncate the file. + let result = tmpfile.as_file().set_len(0); + assert!(result.is_ok()); + }); + + assert!(handle.join().is_ok()); + } } } From 1d9ec572764d1fc74c0f46832318c0ce4e99114a Mon Sep 17 00:00:00 2001 From: 0xmovses <35300528+0xmovses@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:21:01 +0100 Subject: [PATCH 31/54] Refactor Identity to benchmark v2 (#1838) This PR refactors `identity/benchmarkings.rs` to use benchmarking v2. These changes are needed to improve the readability and maintainability of the benchmarking code. Changes were implemented using [this](https://github.com/paritytech/polkadot-sdk/commit/9ec80090f5065ce0e3234886c43bc623e8e60d77) commit as a guide. The logic of the benchmarks remains the same. No known issue to backlink. ## Local Testing To test the new benchmarks: 1. `cargo build --features runtime-benchmarks` 2. `./target/debug/polkadot benchmark pallet --steps=5 --repeat=2 --pallet=pallet_identity --extrinsic='*'` --------- Co-authored-by: Richard Melkonian <movses@richards-mbp.home> Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com> Co-authored-by: command-bot <> Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com> Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> --- substrate/frame/identity/src/benchmarking.rs | 427 ++++++++++++------- 1 file changed, 272 insertions(+), 155 deletions(-) diff --git a/substrate/frame/identity/src/benchmarking.rs b/substrate/frame/identity/src/benchmarking.rs index 4b51d23f6b34f..059de204bbf77 100644 --- a/substrate/frame/identity/src/benchmarking.rs +++ b/substrate/frame/identity/src/benchmarking.rs @@ -22,7 +22,9 @@ use super::*; use crate::Pallet as Identity; -use frame_benchmarking::v1::{account, benchmarks, whitelisted_caller, BenchmarkError}; +use frame_benchmarking::{ + account, impl_benchmark_test_suite, v2::*, whitelisted_caller, BenchmarkError, +}; use frame_support::{ ensure, traits::{EnsureOrigin, Get}, @@ -118,110 +120,128 @@ fn create_identity_info<T: Config>(num_fields: u32) -> IdentityInfo<T::MaxAdditi } } -benchmarks! { - add_registrar { - let r in 1 .. T::MaxRegistrars::get() - 1 => add_registrars::<T>(r)?; +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn add_registrar(r: Linear<1, { T::MaxRegistrars::get() - 1 }>) -> Result<(), BenchmarkError> { + add_registrars::<T>(r)?; ensure!(Registrars::<T>::get().len() as u32 == r, "Registrars not set up correctly."); let origin = T::RegistrarOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; let account = T::Lookup::unlookup(account("registrar", r + 1, SEED)); - }: _<T::RuntimeOrigin>(origin, account) - verify { + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, account); + ensure!(Registrars::<T>::get().len() as u32 == r + 1, "Registrars not added."); + Ok(()) } - set_identity { - let r in 1 .. T::MaxRegistrars::get() => add_registrars::<T>(r)?; - let x in 0 .. T::MaxAdditionalFields::get(); - let caller = { - // The target user - let caller: T::AccountId = whitelisted_caller(); - let caller_lookup = T::Lookup::unlookup(caller.clone()); - let caller_origin: <T as frame_system::Config>::RuntimeOrigin = RawOrigin::Signed(caller.clone()).into(); - let _ = T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value()); - - // Add an initial identity - let initial_info = create_identity_info::<T>(1); - Identity::<T>::set_identity(caller_origin.clone(), Box::new(initial_info.clone()))?; - - // User requests judgement from all the registrars, and they approve - for i in 0..r { - let registrar: T::AccountId = account("registrar", i, SEED); - let registrar_lookup = T::Lookup::unlookup(registrar.clone()); - let balance_to_use = T::Currency::minimum_balance() * 10u32.into(); - let _ = T::Currency::make_free_balance_be(®istrar, balance_to_use); - - Identity::<T>::request_judgement(caller_origin.clone(), i, 10u32.into())?; - Identity::<T>::provide_judgement( - RawOrigin::Signed(registrar).into(), - i, - caller_lookup.clone(), - Judgement::Reasonable, - T::Hashing::hash_of(&initial_info), - )?; - } - caller - }; - }: _(RawOrigin::Signed(caller.clone()), Box::new(create_identity_info::<T>(x))) - verify { + #[benchmark] + fn set_identity( + r: Linear<1, { T::MaxRegistrars::get() }>, + x: Linear<0, { T::MaxAdditionalFields::get() }>, + ) -> Result<(), BenchmarkError> { + add_registrars::<T>(r)?; + + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + let caller_origin: <T as frame_system::Config>::RuntimeOrigin = + RawOrigin::Signed(caller.clone()).into(); + let _ = T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value()); + + // Add an initial identity + let initial_info = create_identity_info::<T>(1); + Identity::<T>::set_identity(caller_origin.clone(), Box::new(initial_info.clone()))?; + + // User requests judgement from all the registrars, and they approve + for i in 0..r { + let registrar: T::AccountId = account("registrar", i, SEED); + let _ = T::Lookup::unlookup(registrar.clone()); + let balance_to_use = T::Currency::minimum_balance() * 10u32.into(); + let _ = T::Currency::make_free_balance_be(®istrar, balance_to_use); + + Identity::<T>::request_judgement(caller_origin.clone(), i, 10u32.into())?; + Identity::<T>::provide_judgement( + RawOrigin::Signed(registrar).into(), + i, + caller_lookup.clone(), + Judgement::Reasonable, + T::Hashing::hash_of(&initial_info), + )?; + } + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), Box::new(create_identity_info::<T>(x))); + assert_last_event::<T>(Event::<T>::IdentitySet { who: caller }.into()); + Ok(()) } // We need to split `set_subs` into two benchmarks to accurately isolate the potential // writes caused by new or old sub accounts. The actual weight should simply be // the sum of these two weights. - set_subs_new { + #[benchmark] + fn set_subs_new(s: Linear<0, { T::MaxSubAccounts::get() }>) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); - // Create a new subs vec with s sub accounts - let s in 0 .. T::MaxSubAccounts::get() => (); + + // Create a new subs vec with sub accounts let subs = create_sub_accounts::<T>(&caller, s)?; ensure!(SubsOf::<T>::get(&caller).1.len() == 0, "Caller already has subs"); - }: set_subs(RawOrigin::Signed(caller.clone()), subs) - verify { + + #[extrinsic_call] + set_subs(RawOrigin::Signed(caller.clone()), subs); + ensure!(SubsOf::<T>::get(&caller).1.len() as u32 == s, "Subs not added"); + Ok(()) } - set_subs_old { + #[benchmark] + fn set_subs_old(p: Linear<0, { T::MaxSubAccounts::get() }>) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); + // Give them p many previous sub accounts. - let p in 0 .. T::MaxSubAccounts::get() => { - let _ = add_sub_accounts::<T>(&caller, p)?; - }; + let _ = add_sub_accounts::<T>(&caller, p)?; + // Remove all subs. let subs = create_sub_accounts::<T>(&caller, 0)?; - ensure!( - SubsOf::<T>::get(&caller).1.len() as u32 == p, - "Caller does have subs", - ); - }: set_subs(RawOrigin::Signed(caller.clone()), subs) - verify { + ensure!(SubsOf::<T>::get(&caller).1.len() as u32 == p, "Caller does have subs",); + + #[extrinsic_call] + set_subs(RawOrigin::Signed(caller.clone()), subs); + ensure!(SubsOf::<T>::get(&caller).1.len() == 0, "Subs not removed"); + Ok(()) } - clear_identity { + #[benchmark] + fn clear_identity( + r: Linear<1, { T::MaxRegistrars::get() }>, + s: Linear<0, { T::MaxSubAccounts::get() }>, + x: Linear<0, { T::MaxAdditionalFields::get() }>, + ) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); - let caller_origin = <T as frame_system::Config>::RuntimeOrigin::from(RawOrigin::Signed(caller.clone())); + let caller_origin = + <T as frame_system::Config>::RuntimeOrigin::from(RawOrigin::Signed(caller.clone())); let caller_lookup = <T::Lookup as StaticLookup>::unlookup(caller.clone()); let _ = T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value()); - let r in 1 .. T::MaxRegistrars::get() => add_registrars::<T>(r)?; - let s in 0 .. T::MaxSubAccounts::get() => { - // Give them s many sub accounts - let caller: T::AccountId = whitelisted_caller(); - let _ = add_sub_accounts::<T>(&caller, s)?; - }; - let x in 0 .. T::MaxAdditionalFields::get(); + // Register the registrars + add_registrars::<T>(r)?; + + // Add sub accounts + let _ = add_sub_accounts::<T>(&caller, s)?; // Create their main identity with x additional fields let info = create_identity_info::<T>(x); - let caller: T::AccountId = whitelisted_caller(); - let caller_origin = <T as frame_system::Config>::RuntimeOrigin::from(RawOrigin::Signed(caller.clone())); Identity::<T>::set_identity(caller_origin.clone(), Box::new(info.clone()))?; // User requests judgement from all the registrars, and they approve for i in 0..r { let registrar: T::AccountId = account("registrar", i, SEED); - let balance_to_use = T::Currency::minimum_balance() * 10u32.into(); + let balance_to_use = T::Currency::minimum_balance() * 10u32.into(); let _ = T::Currency::make_free_balance_be(®istrar, balance_to_use); Identity::<T>::request_judgement(caller_origin.clone(), i, 10u32.into())?; @@ -233,111 +253,175 @@ benchmarks! { T::Hashing::hash_of(&info), )?; } + ensure!(IdentityOf::<T>::contains_key(&caller), "Identity does not exist."); - }: _(RawOrigin::Signed(caller.clone())) - verify { + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone())); + ensure!(!IdentityOf::<T>::contains_key(&caller), "Identity not cleared."); + Ok(()) } - request_judgement { + #[benchmark] + fn request_judgement( + r: Linear<1, { T::MaxRegistrars::get() }>, + x: Linear<0, { T::MaxAdditionalFields::get() }>, + ) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); let _ = T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value()); - let r in 1 .. T::MaxRegistrars::get() => add_registrars::<T>(r)?; - let x in 0 .. T::MaxAdditionalFields::get() => { - // Create their main identity with x additional fields - let info = create_identity_info::<T>(x); - let caller: T::AccountId = whitelisted_caller(); - let caller_origin = <T as frame_system::Config>::RuntimeOrigin::from(RawOrigin::Signed(caller)); - Identity::<T>::set_identity(caller_origin, Box::new(info))?; - }; - }: _(RawOrigin::Signed(caller.clone()), r - 1, 10u32.into()) - verify { - assert_last_event::<T>(Event::<T>::JudgementRequested { who: caller, registrar_index: r-1 }.into()); + // Register the registrars + add_registrars::<T>(r)?; + + // Create their main identity with x additional fields + let info = create_identity_info::<T>(x); + let caller_origin = + <T as frame_system::Config>::RuntimeOrigin::from(RawOrigin::Signed(caller.clone())); + Identity::<T>::set_identity(caller_origin.clone(), Box::new(info))?; + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), r - 1, 10u32.into()); + + assert_last_event::<T>( + Event::<T>::JudgementRequested { who: caller, registrar_index: r - 1 }.into(), + ); + + Ok(()) } - cancel_request { + #[benchmark] + fn cancel_request( + r: Linear<1, { T::MaxRegistrars::get() }>, + x: Linear<0, { T::MaxAdditionalFields::get() }>, + ) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); - let caller_origin = <T as frame_system::Config>::RuntimeOrigin::from(RawOrigin::Signed(caller.clone())); let _ = T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value()); - let r in 1 .. T::MaxRegistrars::get() => add_registrars::<T>(r)?; - let x in 0 .. T::MaxAdditionalFields::get() => { - // Create their main identity with x additional fields - let info = create_identity_info::<T>(x); - let caller: T::AccountId = whitelisted_caller(); - let caller_origin = <T as frame_system::Config>::RuntimeOrigin::from(RawOrigin::Signed(caller)); - Identity::<T>::set_identity(caller_origin, Box::new(info))?; - }; - - Identity::<T>::request_judgement(caller_origin, r - 1, 10u32.into())?; - }: _(RawOrigin::Signed(caller.clone()), r - 1) - verify { - assert_last_event::<T>(Event::<T>::JudgementUnrequested { who: caller, registrar_index: r-1 }.into()); + // Register the registrars + add_registrars::<T>(r)?; + + // Create their main identity with x additional fields + let info = create_identity_info::<T>(x); + let caller_origin = + <T as frame_system::Config>::RuntimeOrigin::from(RawOrigin::Signed(caller.clone())); + Identity::<T>::set_identity(caller_origin.clone(), Box::new(info))?; + + Identity::<T>::request_judgement(caller_origin.clone(), r - 1, 10u32.into())?; + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), r - 1); + + assert_last_event::<T>( + Event::<T>::JudgementUnrequested { who: caller, registrar_index: r - 1 }.into(), + ); + + Ok(()) } - set_fee { + #[benchmark] + fn set_fee(r: Linear<1, { T::MaxRegistrars::get() - 1 }>) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); let caller_lookup = T::Lookup::unlookup(caller.clone()); - let r in 1 .. T::MaxRegistrars::get() - 1 => add_registrars::<T>(r)?; + add_registrars::<T>(r)?; let registrar_origin = T::RegistrarOrigin::try_successful_origin() .expect("RegistrarOrigin has no successful origin required for the benchmark"); Identity::<T>::add_registrar(registrar_origin, caller_lookup)?; + let registrars = Registrars::<T>::get(); ensure!(registrars[r as usize].as_ref().unwrap().fee == 0u32.into(), "Fee already set."); - }: _(RawOrigin::Signed(caller), r, 100u32.into()) - verify { - let registrars = Registrars::<T>::get(); - ensure!(registrars[r as usize].as_ref().unwrap().fee == 100u32.into(), "Fee not changed."); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), r, 100u32.into()); + + let updated_registrars = Registrars::<T>::get(); + ensure!( + updated_registrars[r as usize].as_ref().unwrap().fee == 100u32.into(), + "Fee not changed." + ); + + Ok(()) } - set_account_id { + #[benchmark] + fn set_account_id(r: Linear<1, { T::MaxRegistrars::get() - 1 }>) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); let caller_lookup = T::Lookup::unlookup(caller.clone()); let _ = T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value()); - let r in 1 .. T::MaxRegistrars::get() - 1 => add_registrars::<T>(r)?; + add_registrars::<T>(r)?; let registrar_origin = T::RegistrarOrigin::try_successful_origin() .expect("RegistrarOrigin has no successful origin required for the benchmark"); Identity::<T>::add_registrar(registrar_origin, caller_lookup)?; + let registrars = Registrars::<T>::get(); ensure!(registrars[r as usize].as_ref().unwrap().account == caller, "id not set."); + let new_account = T::Lookup::unlookup(account("new", 0, SEED)); - }: _(RawOrigin::Signed(caller), r, new_account) - verify { - let registrars = Registrars::<T>::get(); - ensure!(registrars[r as usize].as_ref().unwrap().account == account("new", 0, SEED), "id not changed."); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), r, new_account); + + let updated_registrars = Registrars::<T>::get(); + ensure!( + updated_registrars[r as usize].as_ref().unwrap().account == account("new", 0, SEED), + "id not changed." + ); + + Ok(()) } - set_fields { + #[benchmark] + fn set_fields(r: Linear<1, { T::MaxRegistrars::get() - 1 }>) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); let caller_lookup = T::Lookup::unlookup(caller.clone()); let _ = T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value()); - let r in 1 .. T::MaxRegistrars::get() - 1 => add_registrars::<T>(r)?; + add_registrars::<T>(r)?; let registrar_origin = T::RegistrarOrigin::try_successful_origin() .expect("RegistrarOrigin has no successful origin required for the benchmark"); Identity::<T>::add_registrar(registrar_origin, caller_lookup)?; - let fields = IdentityFields( - IdentityField::Display | IdentityField::Legal | IdentityField::Web | IdentityField::Riot - | IdentityField::Email | IdentityField::PgpFingerprint | IdentityField::Image | IdentityField::Twitter - ); - let registrars = Registrars::<T>::get(); - ensure!(registrars[r as usize].as_ref().unwrap().fields == Default::default(), "fields already set."); - }: _(RawOrigin::Signed(caller), r, fields) - verify { + + let fields = + IdentityFields( + IdentityField::Display | + IdentityField::Legal | IdentityField::Web | + IdentityField::Riot | IdentityField::Email | + IdentityField::PgpFingerprint | + IdentityField::Image | IdentityField::Twitter, + ); + let registrars = Registrars::<T>::get(); - ensure!(registrars[r as usize].as_ref().unwrap().fields != Default::default(), "fields not set."); + ensure!( + registrars[r as usize].as_ref().unwrap().fields == Default::default(), + "fields already set." + ); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), r, fields); + + let updated_registrars = Registrars::<T>::get(); + ensure!( + updated_registrars[r as usize].as_ref().unwrap().fields != Default::default(), + "fields not set." + ); + + Ok(()) } - provide_judgement { + #[benchmark] + fn provide_judgement( + r: Linear<1, { T::MaxRegistrars::get() - 1 }>, + x: Linear<0, { T::MaxAdditionalFields::get() }>, + ) -> Result<(), BenchmarkError> { // The user let user: T::AccountId = account("user", r, SEED); - let user_origin = <T as frame_system::Config>::RuntimeOrigin::from(RawOrigin::Signed(user.clone())); + let user_origin = + <T as frame_system::Config>::RuntimeOrigin::from(RawOrigin::Signed(user.clone())); let user_lookup = <T::Lookup as StaticLookup>::unlookup(user.clone()); let _ = T::Currency::make_free_balance_be(&user, BalanceOf::<T>::max_value()); @@ -345,8 +429,7 @@ benchmarks! { let caller_lookup = T::Lookup::unlookup(caller.clone()); let _ = T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value()); - let r in 1 .. T::MaxRegistrars::get() - 1 => add_registrars::<T>(r)?; - let x in 0 .. T::MaxAdditionalFields::get(); + add_registrars::<T>(r)?; let info = create_identity_info::<T>(x); let info_hash = T::Hashing::hash_of(&info); @@ -356,18 +439,28 @@ benchmarks! { .expect("RegistrarOrigin has no successful origin required for the benchmark"); Identity::<T>::add_registrar(registrar_origin, caller_lookup)?; Identity::<T>::request_judgement(user_origin, r, 10u32.into())?; - }: _(RawOrigin::Signed(caller), r, user_lookup, Judgement::Reasonable, info_hash) - verify { - assert_last_event::<T>(Event::<T>::JudgementGiven { target: user, registrar_index: r }.into()) + + #[extrinsic_call] + _(RawOrigin::Signed(caller), r, user_lookup, Judgement::Reasonable, info_hash); + + assert_last_event::<T>( + Event::<T>::JudgementGiven { target: user, registrar_index: r }.into(), + ); + + Ok(()) } - kill_identity { - let r in 1 .. T::MaxRegistrars::get() => add_registrars::<T>(r)?; - let s in 0 .. T::MaxSubAccounts::get(); - let x in 0 .. T::MaxAdditionalFields::get(); + #[benchmark] + fn kill_identity( + r: Linear<1, { T::MaxRegistrars::get() }>, + s: Linear<0, { T::MaxSubAccounts::get() }>, + x: Linear<0, { T::MaxAdditionalFields::get() }>, + ) -> Result<(), BenchmarkError> { + add_registrars::<T>(r)?; let target: T::AccountId = account("target", 0, SEED); - let target_origin: <T as frame_system::Config>::RuntimeOrigin = RawOrigin::Signed(target.clone()).into(); + let target_origin: <T as frame_system::Config>::RuntimeOrigin = + RawOrigin::Signed(target.clone()).into(); let target_lookup = T::Lookup::unlookup(target.clone()); let _ = T::Currency::make_free_balance_be(&target, BalanceOf::<T>::max_value()); @@ -378,7 +471,7 @@ benchmarks! { // User requests judgement from all the registrars, and they approve for i in 0..r { let registrar: T::AccountId = account("registrar", i, SEED); - let balance_to_use = T::Currency::minimum_balance() * 10u32.into(); + let balance_to_use = T::Currency::minimum_balance() * 10u32.into(); let _ = T::Currency::make_free_balance_be(®istrar, balance_to_use); Identity::<T>::request_judgement(target_origin.clone(), i, 10u32.into())?; @@ -390,62 +483,86 @@ benchmarks! { T::Hashing::hash_of(&info), )?; } + ensure!(IdentityOf::<T>::contains_key(&target), "Identity not set"); + let origin = T::ForceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; - }: _<T::RuntimeOrigin>(origin, target_lookup) - verify { + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, target_lookup); + ensure!(!IdentityOf::<T>::contains_key(&target), "Identity not removed"); - } - add_sub { - let s in 0 .. T::MaxSubAccounts::get() - 1; + Ok(()) + } + #[benchmark] + fn add_sub(s: Linear<0, { T::MaxSubAccounts::get() - 1 }>) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); let _ = add_sub_accounts::<T>(&caller, s)?; let sub = account("new_sub", 0, SEED); let data = Data::Raw(vec![0; 32].try_into().unwrap()); + ensure!(SubsOf::<T>::get(&caller).1.len() as u32 == s, "Subs not set."); - }: _(RawOrigin::Signed(caller.clone()), T::Lookup::unlookup(sub), data) - verify { + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), T::Lookup::unlookup(sub), data); + ensure!(SubsOf::<T>::get(&caller).1.len() as u32 == s + 1, "Subs not added."); - } - rename_sub { - let s in 1 .. T::MaxSubAccounts::get(); + Ok(()) + } + #[benchmark] + fn rename_sub(s: Linear<1, { T::MaxSubAccounts::get() }>) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); let (sub, _) = add_sub_accounts::<T>(&caller, s)?.remove(0); let data = Data::Raw(vec![1; 32].try_into().unwrap()); + ensure!(SuperOf::<T>::get(&sub).unwrap().1 != data, "data already set"); - }: _(RawOrigin::Signed(caller), T::Lookup::unlookup(sub.clone()), data.clone()) - verify { + + #[extrinsic_call] + _(RawOrigin::Signed(caller), T::Lookup::unlookup(sub.clone()), data.clone()); + ensure!(SuperOf::<T>::get(&sub).unwrap().1 == data, "data not set"); - } - remove_sub { - let s in 1 .. T::MaxSubAccounts::get(); + Ok(()) + } + #[benchmark] + fn remove_sub(s: Linear<1, { T::MaxSubAccounts::get() }>) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); let (sub, _) = add_sub_accounts::<T>(&caller, s)?.remove(0); ensure!(SuperOf::<T>::contains_key(&sub), "Sub doesn't exists"); - }: _(RawOrigin::Signed(caller), T::Lookup::unlookup(sub.clone())) - verify { + + #[extrinsic_call] + _(RawOrigin::Signed(caller), T::Lookup::unlookup(sub.clone())); + ensure!(!SuperOf::<T>::contains_key(&sub), "Sub not removed"); - } - quit_sub { - let s in 0 .. T::MaxSubAccounts::get() - 1; + Ok(()) + } + #[benchmark] + fn quit_sub(s: Linear<0, { T::MaxSubAccounts::get() - 1 }>) -> Result<(), BenchmarkError> { let caller: T::AccountId = whitelisted_caller(); let sup = account("super", 0, SEED); let _ = add_sub_accounts::<T>(&sup, s)?; let sup_origin = RawOrigin::Signed(sup).into(); - Identity::<T>::add_sub(sup_origin, T::Lookup::unlookup(caller.clone()), Data::Raw(vec![0; 32].try_into().unwrap()))?; + Identity::<T>::add_sub( + sup_origin, + T::Lookup::unlookup(caller.clone()), + Data::Raw(vec![0; 32].try_into().unwrap()), + )?; ensure!(SuperOf::<T>::contains_key(&caller), "Sub doesn't exists"); - }: _(RawOrigin::Signed(caller.clone())) - verify { + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone())); + ensure!(!SuperOf::<T>::contains_key(&caller), "Sub not removed"); + + Ok(()) } impl_benchmark_test_suite!(Identity, crate::tests::new_test_ext(), crate::tests::Test); From 447e7533237d038d43b2de68ff39e0a179503ac4 Mon Sep 17 00:00:00 2001 From: Mira Ressel <mira@parity.io> Date: Wed, 11 Oct 2023 19:49:59 +0200 Subject: [PATCH 32/54] ci: bump ci image to rust 1.73.0 (#1830) Co-authored-by: command-bot <> --- .cargo/config.toml | 8 ++- .gitlab-ci.yml | 2 +- .gitlab/pipeline/build.yml | 2 +- .gitlab/pipeline/test.yml | 1 - polkadot/node/core/backing/src/tests/mod.rs | 4 +- .../node/overseer/examples/minimal-example.rs | 1 - .../cli/src/commands/inspect_node_key.rs | 2 +- substrate/client/network/common/src/role.rs | 4 ++ .../src/protocol/notifications/behaviour.rs | 1 - substrate/frame/contracts/src/wasm/prepare.rs | 3 +- substrate/frame/nfts/src/tests.rs | 2 +- substrate/frame/preimage/src/migration.rs | 2 +- .../deprecated_where_block.stderr | 12 +++- .../tests/derive_no_bound_ui/debug.stderr | 2 +- .../call_argument_invalid_bound.stderr | 2 +- .../call_argument_invalid_bound_2.stderr | 2 +- .../call_argument_invalid_bound_3.stderr | 2 +- ...ev_mode_without_arg_max_encoded_len.stderr | 16 ++--- .../error_does_not_derive_pallet_error.stderr | 16 ++--- .../pallet_ui/event_field_not_member.stderr | 2 +- .../inherent_check_inner_span.stderr | 4 +- ...age_ensure_span_are_ok_on_wrong_gen.stderr | 72 +++++++++---------- ...re_span_are_ok_on_wrong_gen_unnamed.stderr | 72 +++++++++---------- .../pallet_ui/storage_info_unsatisfied.stderr | 16 ++--- .../storage_info_unsatisfied_nmap.stderr | 20 +++--- .../primitives/npos-elections/src/lib.rs | 2 +- .../tests/ui/no_feature_gated_method.stderr | 12 ++++ .../benchmarking-cli/src/pallet/writer.rs | 1 + 28 files changed, 156 insertions(+), 129 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 4796a2c26965c..042dded2fa9b3 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,9 @@ -# +[build] +rustdocflags = [ + "-Dwarnings", + "-Arustdoc::redundant_explicit_links", # stylistic +] + # An auto defined `clippy` feature was introduced, # but it was found to clash with user defined features, # so was renamed to `cargo-clippy`. @@ -30,4 +35,5 @@ rustflags = [ "-Aclippy::derivable_impls", # false positives "-Aclippy::stable_sort_primitive", # prefer stable sort "-Aclippy::extra-unused-type-parameters", # stylistic + "-Aclippy::default_constructed_unit_structs", # stylistic ] diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 61451a9c46201..0d7e7adb95678 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,7 @@ workflow: - if: $CI_COMMIT_BRANCH variables: - CI_IMAGE: !reference [.ci-unified, variables, CI_IMAGE] + CI_IMAGE: !reference [.ci-unified-1.73.0, variables, CI_IMAGE] # BUILDAH_IMAGE is defined in group variables BUILDAH_COMMAND: "buildah --storage-driver overlay2" RELENG_SCRIPTS_BRANCH: "master" diff --git a/.gitlab/pipeline/build.yml b/.gitlab/pipeline/build.yml index 029c0f6a3cddd..fefa3739a9ff4 100644 --- a/.gitlab/pipeline/build.yml +++ b/.gitlab/pipeline/build.yml @@ -91,6 +91,7 @@ build-rustdoc: - .run-immediately variables: SKIP_WASM_BUILD: 1 + RUSTDOCFLAGS: "" artifacts: name: "${CI_JOB_NAME}_${CI_COMMIT_REF_NAME}-doc" when: on_success @@ -99,7 +100,6 @@ build-rustdoc: - ./crate-docs/ script: # FIXME: it fails with `RUSTDOCFLAGS="-Dwarnings"` and `--all-features` - # FIXME: return to stable when https://github.com/rust-lang/rust/issues/96937 gets into stable - time cargo doc --features try-runtime,experimental --workspace --no-deps - rm -f ./target/doc/.lock - mv ./target/doc ./crate-docs diff --git a/.gitlab/pipeline/test.yml b/.gitlab/pipeline/test.yml index e1e8b96bca5d6..12ce2140b1468 100644 --- a/.gitlab/pipeline/test.yml +++ b/.gitlab/pipeline/test.yml @@ -181,7 +181,6 @@ test-rustdoc: - .run-immediately variables: SKIP_WASM_BUILD: 1 - RUSTDOCFLAGS: "-Dwarnings" script: - time cargo doc --workspace --all-features --no-deps allow_failure: true diff --git a/polkadot/node/core/backing/src/tests/mod.rs b/polkadot/node/core/backing/src/tests/mod.rs index bdc8b3fa1af84..35d17f3d905e4 100644 --- a/polkadot/node/core/backing/src/tests/mod.rs +++ b/polkadot/node/core/backing/src/tests/mod.rs @@ -1595,8 +1595,8 @@ fn retry_works() { }, AllMessages::RuntimeApi(RuntimeApiMessage::Request( _, - RuntimeApiRequest::SessionExecutorParams(sess_idx, tx), - )) if sess_idx == 1 => { + RuntimeApiRequest::SessionExecutorParams(1, tx), + )) => { tx.send(Ok(Some(ExecutorParams::default()))).unwrap(); }, msg => { diff --git a/polkadot/node/overseer/examples/minimal-example.rs b/polkadot/node/overseer/examples/minimal-example.rs index e78941776d5ec..cffdfd9f8aa14 100644 --- a/polkadot/node/overseer/examples/minimal-example.rs +++ b/polkadot/node/overseer/examples/minimal-example.rs @@ -163,7 +163,6 @@ fn main() { .unwrap(); let overseer_fut = overseer.run().fuse(); - let timer_stream = timer_stream; pin_mut!(timer_stream); pin_mut!(overseer_fut); diff --git a/substrate/client/cli/src/commands/inspect_node_key.rs b/substrate/client/cli/src/commands/inspect_node_key.rs index 19b5a31ca12c0..6cf025a2d1150 100644 --- a/substrate/client/cli/src/commands/inspect_node_key.rs +++ b/substrate/client/cli/src/commands/inspect_node_key.rs @@ -85,7 +85,7 @@ mod tests { fn inspect_node_key() { let path = tempfile::tempdir().unwrap().into_path().join("node-id").into_os_string(); let path = path.to_str().unwrap(); - let cmd = GenerateNodeKeyCmd::parse_from(&["generate-node-key", "--file", path.clone()]); + let cmd = GenerateNodeKeyCmd::parse_from(&["generate-node-key", "--file", path]); assert!(cmd.run().is_ok()); diff --git a/substrate/client/network/common/src/role.rs b/substrate/client/network/common/src/role.rs index cd43f6655b72c..fd02c00e2324a 100644 --- a/substrate/client/network/common/src/role.rs +++ b/substrate/client/network/common/src/role.rs @@ -16,6 +16,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. +// file-level lint whitelist to avoid problem with bitflags macro below +// TODO: can be dropped after an update to bitflags 2.4 +#![allow(clippy::bad_bit_mask)] + use codec::{self, Encode, EncodeLike, Input, Output}; /// Role that the peer sent to us during the handshake, with the addition of what our local node diff --git a/substrate/client/network/src/protocol/notifications/behaviour.rs b/substrate/client/network/src/protocol/notifications/behaviour.rs index 89513e004c6df..b78f15f8529c6 100644 --- a/substrate/client/network/src/protocol/notifications/behaviour.rs +++ b/substrate/client/network/src/protocol/notifications/behaviour.rs @@ -1423,7 +1423,6 @@ impl NetworkBehaviour for Notifications { let delay_id = self.next_delay_id; self.next_delay_id.0 += 1; let delay = futures_timer::Delay::new(ban_duration); - let peer_id = peer_id; self.delays.push( async move { delay.await; diff --git a/substrate/frame/contracts/src/wasm/prepare.rs b/substrate/frame/contracts/src/wasm/prepare.rs index b129c17e13eca..dfe8c4f8f9b91 100644 --- a/substrate/frame/contracts/src/wasm/prepare.rs +++ b/substrate/frame/contracts/src/wasm/prepare.rs @@ -79,8 +79,7 @@ impl LoadedModule { } let engine = Engine::new(&config); - let module = - Module::new(&engine, code.clone()).map_err(|_| "Can't load the module into wasmi!")?; + let module = Module::new(&engine, code).map_err(|_| "Can't load the module into wasmi!")?; // Return a `LoadedModule` instance with // __valid__ module. diff --git a/substrate/frame/nfts/src/tests.rs b/substrate/frame/nfts/src/tests.rs index 6e264048f11a6..a82fcca015121 100644 --- a/substrate/frame/nfts/src/tests.rs +++ b/substrate/frame/nfts/src/tests.rs @@ -17,7 +17,7 @@ //! Tests for Nfts pallet. -use crate::{mock::*, Event, *}; +use crate::{mock::*, Event, SystemConfig, *}; use enumflags2::BitFlags; use frame_support::{ assert_noop, assert_ok, diff --git a/substrate/frame/preimage/src/migration.rs b/substrate/frame/preimage/src/migration.rs index 821cb01bbaae5..a86109f892a4f 100644 --- a/substrate/frame/preimage/src/migration.rs +++ b/substrate/frame/preimage/src/migration.rs @@ -133,7 +133,7 @@ pub mod v1 { None => OldRequestStatus::Requested { deposit: None, count: 1, len: Some(len) }, }, - v0::OldRequestStatus::Requested(count) if count == 0 => { + v0::OldRequestStatus::Requested(0) => { log::error!(target: TARGET, "preimage has counter of zero: {:?}", hash); continue }, diff --git a/substrate/frame/support/test/tests/construct_runtime_ui/deprecated_where_block.stderr b/substrate/frame/support/test/tests/construct_runtime_ui/deprecated_where_block.stderr index cc2c2e160095d..08954bb6ab5c5 100644 --- a/substrate/frame/support/test/tests/construct_runtime_ui/deprecated_where_block.stderr +++ b/substrate/frame/support/test/tests/construct_runtime_ui/deprecated_where_block.stderr @@ -148,7 +148,11 @@ error[E0277]: the trait bound `Runtime: Config` is not satisfied in `frame_syste | ||_- in this macro invocation ... | | - = note: required because it appears within the type `Event<Runtime>` +note: required because it appears within the type `Event<Runtime>` + --> $WORKSPACE/substrate/frame/system/src/lib.rs + | + | pub enum Event<T: Config> { + | ^^^^^ note: required by a bound in `From` --> $RUST/core/src/convert/mod.rs | @@ -169,7 +173,11 @@ error[E0277]: the trait bound `Runtime: Config` is not satisfied in `frame_syste | ||_- in this macro invocation ... | | - = note: required because it appears within the type `Event<Runtime>` +note: required because it appears within the type `Event<Runtime>` + --> $WORKSPACE/substrate/frame/system/src/lib.rs + | + | pub enum Event<T: Config> { + | ^^^^^ note: required by a bound in `TryInto` --> $RUST/core/src/convert/mod.rs | diff --git a/substrate/frame/support/test/tests/derive_no_bound_ui/debug.stderr b/substrate/frame/support/test/tests/derive_no_bound_ui/debug.stderr index d86292d71b7a1..3291f658f10e1 100644 --- a/substrate/frame/support/test/tests/derive_no_bound_ui/debug.stderr +++ b/substrate/frame/support/test/tests/derive_no_bound_ui/debug.stderr @@ -5,4 +5,4 @@ error[E0277]: `<T as Config>::C` doesn't implement `std::fmt::Debug` | ^ `<T as Config>::C` cannot be formatted using `{:?}` because it doesn't implement `std::fmt::Debug` | = help: the trait `std::fmt::Debug` is not implemented for `<T as Config>::C` - = note: required for the cast from `<T as Config>::C` to the object type `dyn std::fmt::Debug` + = note: required for the cast from `&<T as Config>::C` to `&dyn std::fmt::Debug` diff --git a/substrate/frame/support/test/tests/pallet_ui/call_argument_invalid_bound.stderr b/substrate/frame/support/test/tests/pallet_ui/call_argument_invalid_bound.stderr index c86930f8a64e3..08ea7c0bec3a5 100644 --- a/substrate/frame/support/test/tests/pallet_ui/call_argument_invalid_bound.stderr +++ b/substrate/frame/support/test/tests/pallet_ui/call_argument_invalid_bound.stderr @@ -19,7 +19,7 @@ error[E0277]: `<T as pallet::Config>::Bar` doesn't implement `std::fmt::Debug` | = help: the trait `std::fmt::Debug` is not implemented for `<T as pallet::Config>::Bar` = note: required for `&<T as pallet::Config>::Bar` to implement `std::fmt::Debug` - = note: required for the cast from `&<T as pallet::Config>::Bar` to the object type `dyn std::fmt::Debug` + = note: required for the cast from `&&<T as pallet::Config>::Bar` to `&dyn std::fmt::Debug` error[E0277]: the trait bound `<T as pallet::Config>::Bar: Clone` is not satisfied --> tests/pallet_ui/call_argument_invalid_bound.rs:38:36 diff --git a/substrate/frame/support/test/tests/pallet_ui/call_argument_invalid_bound_2.stderr b/substrate/frame/support/test/tests/pallet_ui/call_argument_invalid_bound_2.stderr index 1b04f44c78feb..80316fcd24897 100644 --- a/substrate/frame/support/test/tests/pallet_ui/call_argument_invalid_bound_2.stderr +++ b/substrate/frame/support/test/tests/pallet_ui/call_argument_invalid_bound_2.stderr @@ -19,7 +19,7 @@ error[E0277]: `<T as pallet::Config>::Bar` doesn't implement `std::fmt::Debug` | = help: the trait `std::fmt::Debug` is not implemented for `<T as pallet::Config>::Bar` = note: required for `&<T as pallet::Config>::Bar` to implement `std::fmt::Debug` - = note: required for the cast from `&<T as pallet::Config>::Bar` to the object type `dyn std::fmt::Debug` + = note: required for the cast from `&&<T as pallet::Config>::Bar` to `&dyn std::fmt::Debug` error[E0277]: the trait bound `<T as pallet::Config>::Bar: Clone` is not satisfied --> tests/pallet_ui/call_argument_invalid_bound_2.rs:38:36 diff --git a/substrate/frame/support/test/tests/pallet_ui/call_argument_invalid_bound_3.stderr b/substrate/frame/support/test/tests/pallet_ui/call_argument_invalid_bound_3.stderr index 7429bce050c28..d45b74bad8428 100644 --- a/substrate/frame/support/test/tests/pallet_ui/call_argument_invalid_bound_3.stderr +++ b/substrate/frame/support/test/tests/pallet_ui/call_argument_invalid_bound_3.stderr @@ -20,7 +20,7 @@ error[E0277]: `Bar` doesn't implement `std::fmt::Debug` = help: the trait `std::fmt::Debug` is not implemented for `Bar` = note: add `#[derive(Debug)]` to `Bar` or manually `impl std::fmt::Debug for Bar` = note: required for `&Bar` to implement `std::fmt::Debug` - = note: required for the cast from `&Bar` to the object type `dyn std::fmt::Debug` + = note: required for the cast from `&&Bar` to `&dyn std::fmt::Debug` help: consider annotating `Bar` with `#[derive(Debug)]` | 34 + #[derive(Debug)] diff --git a/substrate/frame/support/test/tests/pallet_ui/dev_mode_without_arg_max_encoded_len.stderr b/substrate/frame/support/test/tests/pallet_ui/dev_mode_without_arg_max_encoded_len.stderr index 74ee0e4aeba5c..531e8bdffeb0c 100644 --- a/substrate/frame/support/test/tests/pallet_ui/dev_mode_without_arg_max_encoded_len.stderr +++ b/substrate/frame/support/test/tests/pallet_ui/dev_mode_without_arg_max_encoded_len.stderr @@ -30,13 +30,13 @@ error[E0277]: the trait bound `Vec<u8>: MaxEncodedLen` is not satisfied | ^^^^^^ the trait `MaxEncodedLen` is not implemented for `Vec<u8>` | = help: the following other types implement trait `MaxEncodedLen`: - () - (TupleElement0, TupleElement1) - (TupleElement0, TupleElement1, TupleElement2) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6, TupleElement7) + bool + i8 + i16 + i32 + i64 + i128 + u8 + u16 and $N others = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageMyStorage<T>, Vec<u8>>` to implement `StorageInfoTrait` diff --git a/substrate/frame/support/test/tests/pallet_ui/error_does_not_derive_pallet_error.stderr b/substrate/frame/support/test/tests/pallet_ui/error_does_not_derive_pallet_error.stderr index cfa0d465990a2..ea1d0ed99cd39 100644 --- a/substrate/frame/support/test/tests/pallet_ui/error_does_not_derive_pallet_error.stderr +++ b/substrate/frame/support/test/tests/pallet_ui/error_does_not_derive_pallet_error.stderr @@ -5,13 +5,13 @@ error[E0277]: the trait bound `MyError: PalletError` is not satisfied | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PalletError` is not implemented for `MyError` | = help: the following other types implement trait `PalletError`: - () - (TupleElement0, TupleElement1) - (TupleElement0, TupleElement1, TupleElement2) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6, TupleElement7) + bool + i8 + i16 + i32 + i64 + i128 + u8 + u16 and $N others = note: this error originates in the derive macro `frame_support::PalletError` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/substrate/frame/support/test/tests/pallet_ui/event_field_not_member.stderr b/substrate/frame/support/test/tests/pallet_ui/event_field_not_member.stderr index 4df6deafa0df6..fc4a33b721500 100644 --- a/substrate/frame/support/test/tests/pallet_ui/event_field_not_member.stderr +++ b/substrate/frame/support/test/tests/pallet_ui/event_field_not_member.stderr @@ -18,4 +18,4 @@ error[E0277]: `<T as pallet::Config>::Bar` doesn't implement `std::fmt::Debug` | = help: the trait `std::fmt::Debug` is not implemented for `<T as pallet::Config>::Bar` = note: required for `&<T as pallet::Config>::Bar` to implement `std::fmt::Debug` - = note: required for the cast from `&<T as pallet::Config>::Bar` to the object type `dyn std::fmt::Debug` + = note: required for the cast from `&&<T as pallet::Config>::Bar` to `&dyn std::fmt::Debug` diff --git a/substrate/frame/support/test/tests/pallet_ui/inherent_check_inner_span.stderr b/substrate/frame/support/test/tests/pallet_ui/inherent_check_inner_span.stderr index 3a26d1c049541..5ea3be470a068 100644 --- a/substrate/frame/support/test/tests/pallet_ui/inherent_check_inner_span.stderr +++ b/substrate/frame/support/test/tests/pallet_ui/inherent_check_inner_span.stderr @@ -4,8 +4,8 @@ error[E0046]: not all trait items implemented, missing: `Call`, `Error`, `INHERE 36 | impl<T: Config> ProvideInherent for Pallet<T> {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Call`, `Error`, `INHERENT_IDENTIFIER`, `create_inherent`, `is_inherent` in implementation | - = help: implement the missing item: `type Call = Type;` - = help: implement the missing item: `type Error = Type;` + = help: implement the missing item: `type Call = /* Type */;` + = help: implement the missing item: `type Error = /* Type */;` = help: implement the missing item: `const INHERENT_IDENTIFIER: [u8; 8] = value;` = help: implement the missing item: `fn create_inherent(_: &InherentData) -> std::option::Option<<Self as ProvideInherent>::Call> { todo!() }` = help: implement the missing item: `fn is_inherent(_: &<Self as ProvideInherent>::Call) -> bool { todo!() }` diff --git a/substrate/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen.stderr b/substrate/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen.stderr index e290b22a0eaa5..930af1d7fcb30 100644 --- a/substrate/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen.stderr +++ b/substrate/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen.stderr @@ -5,10 +5,10 @@ error[E0277]: the trait bound `Bar: WrapperTypeDecode` is not satisfied | ^^^^^^^^^^^^^^^^^^^^ the trait `WrapperTypeDecode` is not implemented for `Bar` | = help: the following other types implement trait `WrapperTypeDecode`: - Arc<T> Box<T> - Rc<T> frame_support::sp_runtime::sp_application_crypto::sp_core::Bytes + Rc<T> + Arc<T> = note: required for `Bar` to implement `Decode` = note: required for `Bar` to implement `FullCodec` = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo<T>, Bar>` to implement `PartialStorageInfoTrait` @@ -20,14 +20,14 @@ error[E0277]: the trait bound `Bar: EncodeLike` is not satisfied | ^^^^^^^^^^^^^^^^^^^^ the trait `EncodeLike` is not implemented for `Bar` | = help: the following other types implement trait `EncodeLike<T>`: - <&&T as EncodeLike<T>> - <&T as EncodeLike<T>> - <&T as EncodeLike> - <&[(K, V)] as EncodeLike<BTreeMap<LikeK, LikeV>>> - <&[(T,)] as EncodeLike<BTreeSet<LikeT>>> - <&[(T,)] as EncodeLike<BinaryHeap<LikeT>>> - <&[(T,)] as EncodeLike<LinkedList<LikeT>>> - <&[T] as EncodeLike<Vec<U>>> + <bool as EncodeLike> + <i8 as EncodeLike> + <i16 as EncodeLike> + <i32 as EncodeLike> + <i64 as EncodeLike> + <i128 as EncodeLike> + <u8 as EncodeLike> + <u16 as EncodeLike> and $N others = note: required for `Bar` to implement `FullEncode` = note: required for `Bar` to implement `FullCodec` @@ -40,14 +40,14 @@ error[E0277]: the trait bound `Bar: WrapperTypeEncode` is not satisfied | ^^^^^^^^^^^^^^^^^^^^ the trait `WrapperTypeEncode` is not implemented for `Bar` | = help: the following other types implement trait `WrapperTypeEncode`: - &T - &mut T - Arc<T> Box<T> + bytes::bytes::Bytes Cow<'a, T> + parity_scale_codec::Ref<'a, T, U> + frame_support::sp_runtime::sp_application_crypto::sp_core::Bytes Rc<T> + Arc<T> Vec<T> - bytes::bytes::Bytes and $N others = note: required for `Bar` to implement `Encode` = note: required for `Bar` to implement `FullEncode` @@ -61,14 +61,14 @@ error[E0277]: the trait bound `Bar: TypeInfo` is not satisfied | ^^^^^^^ the trait `TypeInfo` is not implemented for `Bar` | = help: the following other types implement trait `TypeInfo`: - &T - &mut T - () - (A, B) - (A, B, C) - (A, B, C, D) - (A, B, C, D, E) - (A, B, C, D, E, F) + bool + char + i8 + i16 + i32 + i64 + i128 + u8 and $N others = note: required for `Bar` to implement `StaticTypeInfo` = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo<T>, Bar>` to implement `StorageEntryMetadataBuilder` @@ -80,10 +80,10 @@ error[E0277]: the trait bound `Bar: WrapperTypeDecode` is not satisfied | ^^^^^^^ the trait `WrapperTypeDecode` is not implemented for `Bar` | = help: the following other types implement trait `WrapperTypeDecode`: - Arc<T> Box<T> - Rc<T> frame_support::sp_runtime::sp_application_crypto::sp_core::Bytes + Rc<T> + Arc<T> = note: required for `Bar` to implement `Decode` = note: required for `Bar` to implement `FullCodec` = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo<T>, Bar>` to implement `StorageEntryMetadataBuilder` @@ -95,14 +95,14 @@ error[E0277]: the trait bound `Bar: EncodeLike` is not satisfied | ^^^^^^^ the trait `EncodeLike` is not implemented for `Bar` | = help: the following other types implement trait `EncodeLike<T>`: - <&&T as EncodeLike<T>> - <&T as EncodeLike<T>> - <&T as EncodeLike> - <&[(K, V)] as EncodeLike<BTreeMap<LikeK, LikeV>>> - <&[(T,)] as EncodeLike<BTreeSet<LikeT>>> - <&[(T,)] as EncodeLike<BinaryHeap<LikeT>>> - <&[(T,)] as EncodeLike<LinkedList<LikeT>>> - <&[T] as EncodeLike<Vec<U>>> + <bool as EncodeLike> + <i8 as EncodeLike> + <i16 as EncodeLike> + <i32 as EncodeLike> + <i64 as EncodeLike> + <i128 as EncodeLike> + <u8 as EncodeLike> + <u16 as EncodeLike> and $N others = note: required for `Bar` to implement `FullEncode` = note: required for `Bar` to implement `FullCodec` @@ -115,14 +115,14 @@ error[E0277]: the trait bound `Bar: WrapperTypeEncode` is not satisfied | ^^^^^^^ the trait `WrapperTypeEncode` is not implemented for `Bar` | = help: the following other types implement trait `WrapperTypeEncode`: - &T - &mut T - Arc<T> Box<T> + bytes::bytes::Bytes Cow<'a, T> + parity_scale_codec::Ref<'a, T, U> + frame_support::sp_runtime::sp_application_crypto::sp_core::Bytes Rc<T> + Arc<T> Vec<T> - bytes::bytes::Bytes and $N others = note: required for `Bar` to implement `Encode` = note: required for `Bar` to implement `FullEncode` diff --git a/substrate/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen_unnamed.stderr b/substrate/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen_unnamed.stderr index 0e3a7c9f1cbfa..79798963c8b1a 100644 --- a/substrate/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen_unnamed.stderr +++ b/substrate/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen_unnamed.stderr @@ -5,10 +5,10 @@ error[E0277]: the trait bound `Bar: WrapperTypeDecode` is not satisfied | ^^^^^^^^^^^^^^^^^^^^ the trait `WrapperTypeDecode` is not implemented for `Bar` | = help: the following other types implement trait `WrapperTypeDecode`: - Arc<T> Box<T> - Rc<T> frame_support::sp_runtime::sp_application_crypto::sp_core::Bytes + Rc<T> + Arc<T> = note: required for `Bar` to implement `Decode` = note: required for `Bar` to implement `FullCodec` = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo<T>, Bar>` to implement `PartialStorageInfoTrait` @@ -20,14 +20,14 @@ error[E0277]: the trait bound `Bar: EncodeLike` is not satisfied | ^^^^^^^^^^^^^^^^^^^^ the trait `EncodeLike` is not implemented for `Bar` | = help: the following other types implement trait `EncodeLike<T>`: - <&&T as EncodeLike<T>> - <&T as EncodeLike<T>> - <&T as EncodeLike> - <&[(K, V)] as EncodeLike<BTreeMap<LikeK, LikeV>>> - <&[(T,)] as EncodeLike<BTreeSet<LikeT>>> - <&[(T,)] as EncodeLike<BinaryHeap<LikeT>>> - <&[(T,)] as EncodeLike<LinkedList<LikeT>>> - <&[T] as EncodeLike<Vec<U>>> + <bool as EncodeLike> + <i8 as EncodeLike> + <i16 as EncodeLike> + <i32 as EncodeLike> + <i64 as EncodeLike> + <i128 as EncodeLike> + <u8 as EncodeLike> + <u16 as EncodeLike> and $N others = note: required for `Bar` to implement `FullEncode` = note: required for `Bar` to implement `FullCodec` @@ -40,14 +40,14 @@ error[E0277]: the trait bound `Bar: WrapperTypeEncode` is not satisfied | ^^^^^^^^^^^^^^^^^^^^ the trait `WrapperTypeEncode` is not implemented for `Bar` | = help: the following other types implement trait `WrapperTypeEncode`: - &T - &mut T - Arc<T> Box<T> + bytes::bytes::Bytes Cow<'a, T> + parity_scale_codec::Ref<'a, T, U> + frame_support::sp_runtime::sp_application_crypto::sp_core::Bytes Rc<T> + Arc<T> Vec<T> - bytes::bytes::Bytes and $N others = note: required for `Bar` to implement `Encode` = note: required for `Bar` to implement `FullEncode` @@ -61,14 +61,14 @@ error[E0277]: the trait bound `Bar: TypeInfo` is not satisfied | ^^^^^^^ the trait `TypeInfo` is not implemented for `Bar` | = help: the following other types implement trait `TypeInfo`: - &T - &mut T - () - (A, B) - (A, B, C) - (A, B, C, D) - (A, B, C, D, E) - (A, B, C, D, E, F) + bool + char + i8 + i16 + i32 + i64 + i128 + u8 and $N others = note: required for `Bar` to implement `StaticTypeInfo` = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo<T>, Bar>` to implement `StorageEntryMetadataBuilder` @@ -80,10 +80,10 @@ error[E0277]: the trait bound `Bar: WrapperTypeDecode` is not satisfied | ^^^^^^^ the trait `WrapperTypeDecode` is not implemented for `Bar` | = help: the following other types implement trait `WrapperTypeDecode`: - Arc<T> Box<T> - Rc<T> frame_support::sp_runtime::sp_application_crypto::sp_core::Bytes + Rc<T> + Arc<T> = note: required for `Bar` to implement `Decode` = note: required for `Bar` to implement `FullCodec` = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo<T>, Bar>` to implement `StorageEntryMetadataBuilder` @@ -95,14 +95,14 @@ error[E0277]: the trait bound `Bar: EncodeLike` is not satisfied | ^^^^^^^ the trait `EncodeLike` is not implemented for `Bar` | = help: the following other types implement trait `EncodeLike<T>`: - <&&T as EncodeLike<T>> - <&T as EncodeLike<T>> - <&T as EncodeLike> - <&[(K, V)] as EncodeLike<BTreeMap<LikeK, LikeV>>> - <&[(T,)] as EncodeLike<BTreeSet<LikeT>>> - <&[(T,)] as EncodeLike<BinaryHeap<LikeT>>> - <&[(T,)] as EncodeLike<LinkedList<LikeT>>> - <&[T] as EncodeLike<Vec<U>>> + <bool as EncodeLike> + <i8 as EncodeLike> + <i16 as EncodeLike> + <i32 as EncodeLike> + <i64 as EncodeLike> + <i128 as EncodeLike> + <u8 as EncodeLike> + <u16 as EncodeLike> and $N others = note: required for `Bar` to implement `FullEncode` = note: required for `Bar` to implement `FullCodec` @@ -115,14 +115,14 @@ error[E0277]: the trait bound `Bar: WrapperTypeEncode` is not satisfied | ^^^^^^^ the trait `WrapperTypeEncode` is not implemented for `Bar` | = help: the following other types implement trait `WrapperTypeEncode`: - &T - &mut T - Arc<T> Box<T> + bytes::bytes::Bytes Cow<'a, T> + parity_scale_codec::Ref<'a, T, U> + frame_support::sp_runtime::sp_application_crypto::sp_core::Bytes Rc<T> + Arc<T> Vec<T> - bytes::bytes::Bytes and $N others = note: required for `Bar` to implement `Encode` = note: required for `Bar` to implement `FullEncode` diff --git a/substrate/frame/support/test/tests/pallet_ui/storage_info_unsatisfied.stderr b/substrate/frame/support/test/tests/pallet_ui/storage_info_unsatisfied.stderr index 9a31e4b6bdf46..e04de98800ec2 100644 --- a/substrate/frame/support/test/tests/pallet_ui/storage_info_unsatisfied.stderr +++ b/substrate/frame/support/test/tests/pallet_ui/storage_info_unsatisfied.stderr @@ -5,13 +5,13 @@ error[E0277]: the trait bound `Bar: MaxEncodedLen` is not satisfied | ^^^^^^ the trait `MaxEncodedLen` is not implemented for `Bar` | = help: the following other types implement trait `MaxEncodedLen`: - () - (TupleElement0, TupleElement1) - (TupleElement0, TupleElement1, TupleElement2) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6, TupleElement7) + bool + i8 + i16 + i32 + i64 + i128 + u8 + u16 and $N others = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo<T>, Bar>` to implement `StorageInfoTrait` diff --git a/substrate/frame/support/test/tests/pallet_ui/storage_info_unsatisfied_nmap.stderr b/substrate/frame/support/test/tests/pallet_ui/storage_info_unsatisfied_nmap.stderr index cdcd1b401f801..31fe3b5733896 100644 --- a/substrate/frame/support/test/tests/pallet_ui/storage_info_unsatisfied_nmap.stderr +++ b/substrate/frame/support/test/tests/pallet_ui/storage_info_unsatisfied_nmap.stderr @@ -5,14 +5,14 @@ error[E0277]: the trait bound `Bar: MaxEncodedLen` is not satisfied | ^^^^^^ the trait `MaxEncodedLen` is not implemented for `Bar` | = help: the following other types implement trait `MaxEncodedLen`: - () - (TupleElement0, TupleElement1) - (TupleElement0, TupleElement1, TupleElement2) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6) - (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6, TupleElement7) + bool + i8 + i16 + i32 + i64 + i128 + u8 + u16 and $N others - = note: required for `Key<frame_support::Twox64Concat, Bar>` to implement `KeyGeneratorMaxEncodedLen` - = note: required for `frame_support::pallet_prelude::StorageNMap<_GeneratedPrefixForStorageFoo<T>, Key<frame_support::Twox64Concat, Bar>, u32>` to implement `StorageInfoTrait` + = note: required for `NMapKey<frame_support::Twox64Concat, Bar>` to implement `KeyGeneratorMaxEncodedLen` + = note: required for `frame_support::pallet_prelude::StorageNMap<_GeneratedPrefixForStorageFoo<T>, NMapKey<frame_support::Twox64Concat, Bar>, u32>` to implement `StorageInfoTrait` diff --git a/substrate/primitives/npos-elections/src/lib.rs b/substrate/primitives/npos-elections/src/lib.rs index 0afe1ec5bb692..62ae050211482 100644 --- a/substrate/primitives/npos-elections/src/lib.rs +++ b/substrate/primitives/npos-elections/src/lib.rs @@ -19,7 +19,7 @@ //! - [`seq_phragmen`]: Implements the Phragmén Sequential Method. An un-ranked, relatively fast //! election method that ensures PJR, but does not provide a constant factor approximation of the //! maximin problem. -//! - [`phragmms`](phragmms::phragmms): Implements a hybrid approach inspired by Phragmén which is +//! - [`ghragmms`](phragmms::phragmms()): Implements a hybrid approach inspired by Phragmén which is //! executed faster but it can achieve a constant factor approximation of the maximin problem, //! similar to that of the MMS algorithm. //! - [`balance`](balancing::balance): Implements the star balancing algorithm. This iterative diff --git a/substrate/primitives/runtime-interface/tests/ui/no_feature_gated_method.stderr b/substrate/primitives/runtime-interface/tests/ui/no_feature_gated_method.stderr index 23e671f6ce3f3..10012ede793de 100644 --- a/substrate/primitives/runtime-interface/tests/ui/no_feature_gated_method.stderr +++ b/substrate/primitives/runtime-interface/tests/ui/no_feature_gated_method.stderr @@ -3,3 +3,15 @@ error[E0425]: cannot find function `bar` in module `test` | 33 | test::bar(); | ^^^ not found in `test` + | +note: found an item that was configured out + --> tests/ui/no_feature_gated_method.rs:25:5 + | +25 | fn bar() {} + | ^^^ + = note: the item is gated behind the `bar-feature` feature +note: found an item that was configured out + --> tests/ui/no_feature_gated_method.rs:25:5 + | +25 | fn bar() {} + | ^^^ diff --git a/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs b/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs index 69c95d13c0985..9493a693bbed3 100644 --- a/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs +++ b/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs @@ -779,6 +779,7 @@ fn worst_case_pov( /// A simple match statement which outputs the log 16 of some value. fn easy_log_16(i: u32) -> u32 { + #[allow(clippy::redundant_guards)] match i { i if i == 0 => 0, i if i <= 16 => 1, From b5aee6e23f2d397cf3da389a821b4f344a8ae65c Mon Sep 17 00:00:00 2001 From: Mira Ressel <mira@parity.io> Date: Wed, 11 Oct 2023 21:20:46 +0200 Subject: [PATCH 33/54] ci: set CI_IMAGE back to (now updated) .ci-unified (#1854) --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0d7e7adb95678..61451a9c46201 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,7 @@ workflow: - if: $CI_COMMIT_BRANCH variables: - CI_IMAGE: !reference [.ci-unified-1.73.0, variables, CI_IMAGE] + CI_IMAGE: !reference [.ci-unified, variables, CI_IMAGE] # BUILDAH_IMAGE is defined in group variables BUILDAH_COMMAND: "buildah --storage-driver overlay2" RELENG_SCRIPTS_BRANCH: "master" From 70d4907a32708e921de78e18fc9c7e3dbcfe4fe1 Mon Sep 17 00:00:00 2001 From: Sam Elamin <hussam.elamin@gmail.com> Date: Thu, 12 Oct 2023 10:48:32 +0100 Subject: [PATCH 34/54] allow treasury to do reserve asset transfers (#1447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pr resolves https://github.com/paritytech/polkadot-sdk/issues/1428. *Added only to Kusama for now* I did raise it [here](https://github.com/polkadot-fellows/runtimes/pull/19) and we discussed creating a chopsticks test to run an end-to-end test however, to do that I will need a build agent/custom runner that is powerful enough to run the build I will be doing that separately as I still think having chopsticks test your runtime with each commit will be very powerful and extremely useful for the ecosystem For now I have used XCM simulator and replicated what the other reserve tests do --------- Co-authored-by: Gavin Wood <github@gavwood.com> Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com> Co-authored-by: Branislav Kontur <bkontur@gmail.com> Co-authored-by: Bastian Köcher <git@kchr.de> --- polkadot/xcm/xcm-builder/src/lib.rs | 6 +- .../xcm-builder/src/location_conversion.rs | 105 +++++++++++++++++- 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/polkadot/xcm/xcm-builder/src/lib.rs b/polkadot/xcm/xcm-builder/src/lib.rs index 26f2aadadf3de..9fdd55d9f9274 100644 --- a/polkadot/xcm/xcm-builder/src/lib.rs +++ b/polkadot/xcm/xcm-builder/src/lib.rs @@ -33,9 +33,9 @@ pub use location_conversion::{ Account32Hash, AccountId32Aliases, AccountKey20Aliases, AliasesIntoAccountId32, ChildParachainConvertsVia, DescribeAccountId32Terminal, DescribeAccountIdTerminal, DescribeAccountKey20Terminal, DescribeAllTerminal, DescribeBodyTerminal, DescribeFamily, - DescribeLocation, DescribePalletTerminal, DescribeTerminus, GlobalConsensusConvertsFor, - GlobalConsensusParachainConvertsFor, HashedDescription, ParentIsPreset, - SiblingParachainConvertsVia, + DescribeLocation, DescribePalletTerminal, DescribeTerminus, DescribeTreasuryVoiceTerminal, + GlobalConsensusConvertsFor, GlobalConsensusParachainConvertsFor, HashedDescription, + LocalTreasuryVoiceConvertsVia, ParentIsPreset, SiblingParachainConvertsVia, }; mod origin_conversion; diff --git a/polkadot/xcm/xcm-builder/src/location_conversion.rs b/polkadot/xcm/xcm-builder/src/location_conversion.rs index 4a5fbbb123be1..25d16f7eb8ccc 100644 --- a/polkadot/xcm/xcm-builder/src/location_conversion.rs +++ b/polkadot/xcm/xcm-builder/src/location_conversion.rs @@ -84,6 +84,20 @@ impl DescribeLocation for DescribeAccountKey20Terminal { } } +/// Create a description of the remote treasury `location` if possible. No two locations should have +/// the same descriptor. +pub struct DescribeTreasuryVoiceTerminal; + +impl DescribeLocation for DescribeTreasuryVoiceTerminal { + fn describe_location(l: &MultiLocation) -> Option<Vec<u8>> { + match (l.parents, &l.interior) { + (0, X1(Plurality { id: BodyId::Treasury, part: BodyPart::Voice })) => + Some((b"Treasury", b"Voice").encode()), + _ => None, + } + } +} + pub type DescribeAccountIdTerminal = (DescribeAccountId32Terminal, DescribeAccountKey20Terminal); pub struct DescribeBodyTerminal; @@ -101,6 +115,7 @@ pub type DescribeAllTerminal = ( DescribePalletTerminal, DescribeAccountId32Terminal, DescribeAccountKey20Terminal, + DescribeTreasuryVoiceTerminal, DescribeBodyTerminal, ); @@ -328,6 +343,25 @@ impl<Network: Get<Option<NetworkId>>, AccountId: From<[u8; 32]> + Into<[u8; 32]> } } +/// Returns specified `TreasuryAccount` as `AccountId32` if passed `location` matches Treasury +/// plurality. +pub struct LocalTreasuryVoiceConvertsVia<TreasuryAccount, AccountId>( + PhantomData<(TreasuryAccount, AccountId)>, +); +impl<TreasuryAccount: Get<AccountId>, AccountId: From<[u8; 32]> + Into<[u8; 32]> + Clone> + ConvertLocation<AccountId> for LocalTreasuryVoiceConvertsVia<TreasuryAccount, AccountId> +{ + fn convert_location(location: &MultiLocation) -> Option<AccountId> { + match *location { + MultiLocation { + parents: 0, + interior: X1(Plurality { id: BodyId::Treasury, part: BodyPart::Voice }), + } => Some((TreasuryAccount::get().into() as [u8; 32]).into()), + _ => None, + } + } +} + /// Conversion implementation which converts from a `[u8; 32]`-based `AccountId` into a /// `MultiLocation` consisting solely of a `AccountId32` junction with a fixed value for its /// network (provided by `Network`) and the `AccountId`'s `[u8; 32]` datum for the `id`. @@ -442,10 +476,13 @@ impl<UniversalLocation, AccountId> #[cfg(test)] mod tests { use super::*; - + use primitives::AccountId; pub type ForeignChainAliasAccount<AccountId> = HashedDescription<AccountId, LegacyDescribeForeignChainAccount>; + pub type ForeignChainAliasTreasuryAccount<AccountId> = + HashedDescription<AccountId, DescribeFamily<DescribeTreasuryVoiceTerminal>>; + use frame_support::parameter_types; use xcm::latest::Junction; @@ -936,4 +973,70 @@ mod tests { }; assert!(ForeignChainAliasAccount::<[u8; 32]>::convert_location(&mul).is_none()); } + + #[test] + fn remote_account_convert_on_para_sending_from_remote_para_treasury() { + let relay_treasury_to_para_location = MultiLocation { + parents: 1, + interior: X1(Plurality { id: BodyId::Treasury, part: BodyPart::Voice }), + }; + let actual_description = ForeignChainAliasTreasuryAccount::<[u8; 32]>::convert_location( + &relay_treasury_to_para_location, + ) + .unwrap(); + + assert_eq!( + [ + 18, 84, 93, 74, 187, 212, 254, 71, 192, 127, 112, 51, 3, 42, 54, 24, 220, 185, 161, + 67, 205, 154, 108, 116, 108, 166, 226, 211, 29, 11, 244, 115 + ], + actual_description + ); + + let para_to_para_treasury_location = MultiLocation { + parents: 1, + interior: X2( + Parachain(1001), + Plurality { id: BodyId::Treasury, part: BodyPart::Voice }, + ), + }; + let actual_description = ForeignChainAliasTreasuryAccount::<[u8; 32]>::convert_location( + ¶_to_para_treasury_location, + ) + .unwrap(); + + assert_eq!( + [ + 202, 52, 249, 30, 7, 99, 135, 128, 153, 139, 176, 141, 138, 234, 163, 150, 7, 36, + 204, 92, 220, 137, 87, 57, 73, 91, 243, 189, 245, 200, 217, 204 + ], + actual_description + ); + } + + #[test] + fn local_account_convert_on_para_from_relay_treasury() { + let location = MultiLocation { + parents: 0, + interior: X1(Plurality { id: BodyId::Treasury, part: BodyPart::Voice }), + }; + + parameter_types! { + pub TreasuryAccountId: AccountId = AccountId::new([42u8; 32]); + } + + let actual_description = + LocalTreasuryVoiceConvertsVia::<TreasuryAccountId, [u8; 32]>::convert_location( + &location, + ) + .unwrap(); + + assert_eq!( + [ + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42 + ], + actual_description + ); + } } From f0e6d2ad52a8edb277f4144dee620eb1ce8a8a94 Mon Sep 17 00:00:00 2001 From: Kevin Krone <kevin@parity.io> Date: Thu, 12 Oct 2023 14:53:01 +0200 Subject: [PATCH 35/54] Adding `try_state` hook for `Treasury` pallet (#1820) This PR adds a `try_state` hook for the `Treasury` pallet. Part of #239. --- substrate/frame/treasury/src/benchmarking.rs | 6 +- substrate/frame/treasury/src/lib.rs | 90 ++++++- substrate/frame/treasury/src/tests.rs | 252 ++++++++++++++++--- 3 files changed, 308 insertions(+), 40 deletions(-) diff --git a/substrate/frame/treasury/src/benchmarking.rs b/substrate/frame/treasury/src/benchmarking.rs index f5f73ea8ddabd..61fe29dafcae5 100644 --- a/substrate/frame/treasury/src/benchmarking.rs +++ b/substrate/frame/treasury/src/benchmarking.rs @@ -336,5 +336,9 @@ mod benchmarks { Ok(()) } - impl_benchmark_test_suite!(Treasury, crate::tests::new_test_ext(), crate::tests::Test); + impl_benchmark_test_suite!( + Treasury, + crate::tests::ExtBuilder::default().build(), + crate::tests::Test + ); } diff --git a/substrate/frame/treasury/src/lib.rs b/substrate/frame/treasury/src/lib.rs index b2b3a8801c156..5e429d3914bd0 100644 --- a/substrate/frame/treasury/src/lib.rs +++ b/substrate/frame/treasury/src/lib.rs @@ -89,7 +89,8 @@ use sp_runtime::{ use sp_std::{collections::btree_map::BTreeMap, prelude::*}; use frame_support::{ - print, + dispatch::{DispatchResult, DispatchResultWithPostInfo}, + ensure, print, traits::{ tokens::Pay, Currency, ExistenceRequirement::KeepAlive, Get, Imbalance, OnUnbalanced, ReservableCurrency, WithdrawReasons, @@ -456,6 +457,14 @@ pub mod pallet { Weight::zero() } } + + #[cfg(feature = "try-runtime")] + fn try_state( + _: frame_system::pallet_prelude::BlockNumberFor<T>, + ) -> Result<(), sp_runtime::TryRuntimeError> { + Self::do_try_state()?; + Ok(()) + } } #[derive(Default)] @@ -1020,6 +1029,85 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> { // Must never be less than 0 but better be safe. .saturating_sub(T::Currency::minimum_balance()) } + + /// Ensure the correctness of the state of this pallet. + #[cfg(any(feature = "try-runtime", test))] + fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> { + Self::try_state_proposals()?; + Self::try_state_spends()?; + + Ok(()) + } + + /// ### Invariants of proposal storage items + /// + /// 1. [`ProposalCount`] >= Number of elements in [`Proposals`]. + /// 2. Each entry in [`Proposals`] should be saved under a key stricly less than current + /// [`ProposalCount`]. + /// 3. Each [`ProposalIndex`] contained in [`Approvals`] should exist in [`Proposals`]. + /// Note, that this automatically implies [`Approvals`].count() <= [`Proposals`].count(). + #[cfg(any(feature = "try-runtime", test))] + fn try_state_proposals() -> Result<(), sp_runtime::TryRuntimeError> { + let current_proposal_count = ProposalCount::<T, I>::get(); + ensure!( + current_proposal_count as usize >= Proposals::<T, I>::iter().count(), + "Actual number of proposals exceeds `ProposalCount`." + ); + + Proposals::<T, I>::iter_keys().try_for_each(|proposal_index| -> DispatchResult { + ensure!( + current_proposal_count as u32 > proposal_index, + "`ProposalCount` should by strictly greater than any ProposalIndex used as a key for `Proposals`." + ); + Ok(()) + })?; + + Approvals::<T, I>::get() + .iter() + .try_for_each(|proposal_index| -> DispatchResult { + ensure!( + Proposals::<T, I>::contains_key(proposal_index), + "Proposal indices in `Approvals` must also be contained in `Proposals`." + ); + Ok(()) + })?; + + Ok(()) + } + + /// ## Invariants of spend storage items + /// + /// 1. [`SpendCount`] >= Number of elements in [`Spends`]. + /// 2. Each entry in [`Spends`] should be saved under a key stricly less than current + /// [`SpendCount`]. + /// 3. For each spend entry contained in [`Spends`] we should have spend.expire_at + /// > spend.valid_from. + #[cfg(any(feature = "try-runtime", test))] + fn try_state_spends() -> Result<(), sp_runtime::TryRuntimeError> { + let current_spend_count = SpendCount::<T, I>::get(); + ensure!( + current_spend_count as usize >= Spends::<T, I>::iter().count(), + "Actual number of spends exceeds `SpendCount`." + ); + + Spends::<T, I>::iter_keys().try_for_each(|spend_index| -> DispatchResult { + ensure!( + current_spend_count > spend_index, + "`SpendCount` should by strictly greater than any SpendIndex used as a key for `Spends`." + ); + Ok(()) + })?; + + Spends::<T, I>::iter().try_for_each(|(_index, spend)| -> DispatchResult { + ensure!( + spend.valid_from < spend.expire_at, + "Spend cannot expire before it becomes valid." + ); + Ok(()) + })?; + + Ok(()) + } } impl<T: Config<I>, I: 'static> OnUnbalanced<NegativeImbalanceOf<T, I>> for Pallet<T, I> { diff --git a/substrate/frame/treasury/src/tests.rs b/substrate/frame/treasury/src/tests.rs index 4bb00547d9f28..1748189723edf 100644 --- a/substrate/frame/treasury/src/tests.rs +++ b/substrate/frame/treasury/src/tests.rs @@ -217,16 +217,28 @@ impl Config for Test { type BenchmarkHelper = (); } -pub fn new_test_ext() -> sp_io::TestExternalities { - let mut t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap(); - pallet_balances::GenesisConfig::<Test> { - // Total issuance will be 200 with treasury account initialized at ED. - balances: vec![(0, 100), (1, 98), (2, 1)], +pub struct ExtBuilder {} + +impl Default for ExtBuilder { + fn default() -> Self { + Self {} + } +} + +impl ExtBuilder { + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap(); + pallet_balances::GenesisConfig::<Test> { + // Total issuance will be 200 with treasury account initialized at ED. + balances: vec![(0, 100), (1, 98), (2, 1)], + } + .assimilate_storage(&mut t) + .unwrap(); + crate::GenesisConfig::<Test>::default().assimilate_storage(&mut t).unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext } - .assimilate_storage(&mut t) - .unwrap(); - crate::GenesisConfig::<Test>::default().assimilate_storage(&mut t).unwrap(); - t.into() } fn get_payment_id(i: SpendIndex) -> Option<u64> { @@ -239,7 +251,7 @@ fn get_payment_id(i: SpendIndex) -> Option<u64> { #[test] fn genesis_config_works() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { assert_eq!(Treasury::pot(), 0); assert_eq!(Treasury::proposal_count(), 0); }); @@ -247,7 +259,7 @@ fn genesis_config_works() { #[test] fn spend_local_origin_permissioning_works() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { assert_noop!(Treasury::spend_local(RuntimeOrigin::signed(1), 1, 1), BadOrigin); assert_noop!( Treasury::spend_local(RuntimeOrigin::signed(10), 6, 1), @@ -271,7 +283,7 @@ fn spend_local_origin_permissioning_works() { #[docify::export] #[test] fn spend_local_origin_works() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { // Check that accumulate works when we have Some value in Dummy already. Balances::make_free_balance_be(&Treasury::account_id(), 101); // approve spend of some amount to beneficiary `6`. @@ -295,7 +307,7 @@ fn spend_local_origin_works() { #[test] fn minting_works() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { // Check that accumulate works when we have Some value in Dummy already. Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_eq!(Treasury::pot(), 100); @@ -304,7 +316,7 @@ fn minting_works() { #[test] fn spend_proposal_takes_min_deposit() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { assert_ok!({ #[allow(deprecated)] Treasury::propose_spend(RuntimeOrigin::signed(0), 1, 3) @@ -316,7 +328,7 @@ fn spend_proposal_takes_min_deposit() { #[test] fn spend_proposal_takes_proportional_deposit() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { assert_ok!({ #[allow(deprecated)] Treasury::propose_spend(RuntimeOrigin::signed(0), 100, 3) @@ -328,7 +340,7 @@ fn spend_proposal_takes_proportional_deposit() { #[test] fn spend_proposal_fails_when_proposer_poor() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { assert_noop!( { #[allow(deprecated)] @@ -341,7 +353,7 @@ fn spend_proposal_fails_when_proposer_poor() { #[test] fn accepted_spend_proposal_ignored_outside_spend_period() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!({ @@ -361,7 +373,7 @@ fn accepted_spend_proposal_ignored_outside_spend_period() { #[test] fn unused_pot_should_diminish() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { let init_total_issuance = Balances::total_issuance(); Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_eq!(Balances::total_issuance(), init_total_issuance + 100); @@ -374,7 +386,7 @@ fn unused_pot_should_diminish() { #[test] fn rejected_spend_proposal_ignored_on_spend_period() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!({ @@ -394,7 +406,7 @@ fn rejected_spend_proposal_ignored_on_spend_period() { #[test] fn reject_already_rejected_spend_proposal_fails() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!({ @@ -417,7 +429,7 @@ fn reject_already_rejected_spend_proposal_fails() { #[test] fn reject_non_existent_spend_proposal_fails() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { assert_noop!( { #[allow(deprecated)] @@ -430,7 +442,7 @@ fn reject_non_existent_spend_proposal_fails() { #[test] fn accept_non_existent_spend_proposal_fails() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { assert_noop!( { #[allow(deprecated)] @@ -443,7 +455,7 @@ fn accept_non_existent_spend_proposal_fails() { #[test] fn accept_already_rejected_spend_proposal_fails() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!({ @@ -466,7 +478,7 @@ fn accept_already_rejected_spend_proposal_fails() { #[test] fn accepted_spend_proposal_enacted_on_spend_period() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_eq!(Treasury::pot(), 100); @@ -487,7 +499,7 @@ fn accepted_spend_proposal_enacted_on_spend_period() { #[test] fn pot_underflow_should_not_diminish() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_eq!(Treasury::pot(), 100); @@ -514,7 +526,7 @@ fn pot_underflow_should_not_diminish() { // i.e. pot should not include existential deposit needed for account survival. #[test] fn treasury_account_doesnt_get_deleted() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_eq!(Treasury::pot(), 100); let treasury_balance = Balances::free_balance(&Treasury::account_id()); @@ -613,7 +625,7 @@ fn genesis_funding_works() { #[test] fn max_approvals_limited() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { Balances::make_free_balance_be(&Treasury::account_id(), u64::MAX); Balances::make_free_balance_be(&0, u64::MAX); @@ -645,7 +657,7 @@ fn max_approvals_limited() { #[test] fn remove_already_removed_approval_fails() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { Balances::make_free_balance_be(&Treasury::account_id(), 101); assert_ok!({ @@ -669,7 +681,7 @@ fn remove_already_removed_approval_fails() { #[test] fn spending_local_in_batch_respects_max_total() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { // Respect the `max_total` for the given origin. assert_ok!(RuntimeCall::from(UtilityCall::batch_all { calls: vec![ @@ -694,7 +706,7 @@ fn spending_local_in_batch_respects_max_total() { #[test] fn spending_in_batch_respects_max_total() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { // Respect the `max_total` for the given origin. assert_ok!(RuntimeCall::from(UtilityCall::batch_all { calls: vec![ @@ -739,7 +751,7 @@ fn spending_in_batch_respects_max_total() { #[test] fn spend_origin_works() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None)); assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); assert_noop!( @@ -764,7 +776,7 @@ fn spend_origin_works() { #[test] fn spend_works() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { System::set_block_number(1); assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); @@ -796,7 +808,7 @@ fn spend_works() { #[test] fn spend_expires() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { assert_eq!(<Test as Config>::PayoutPeriod::get(), 5); // spend `0` expires in 5 blocks after the creating. @@ -816,7 +828,7 @@ fn spend_expires() { #[docify::export] #[test] fn spend_payout_works() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { System::set_block_number(1); // approve a `2` coins spend of asset `1` to beneficiary `6`, the spend valid from now. assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); @@ -838,7 +850,7 @@ fn spend_payout_works() { #[test] fn payout_retry_works() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { System::set_block_number(1); assert_ok!(Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 2, Box::new(6), None)); assert_ok!(Treasury::payout(RuntimeOrigin::signed(1), 0)); @@ -863,7 +875,7 @@ fn payout_retry_works() { #[test] fn spend_valid_from_works() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { assert_eq!(<Test as Config>::PayoutPeriod::get(), 5); System::set_block_number(1); @@ -895,7 +907,7 @@ fn spend_valid_from_works() { #[test] fn void_spend_works() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { System::set_block_number(1); // spend cannot be voided if already attempted. assert_ok!(Treasury::spend( @@ -926,7 +938,7 @@ fn void_spend_works() { #[test] fn check_status_works() { - new_test_ext().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { assert_eq!(<Test as Config>::PayoutPeriod::get(), 5); System::set_block_number(1); @@ -984,3 +996,167 @@ fn check_status_works() { System::assert_last_event(Event::<Test, _>::SpendProcessed { index: 4 }.into()); }); } + +#[test] +fn try_state_proposals_invariant_1_works() { + ExtBuilder::default().build().execute_with(|| { + use frame_support::pallet_prelude::DispatchError::Other; + // Add a proposal using `propose_spend` + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 1, 3) + }); + assert_eq!(Proposals::<Test>::iter().count(), 1); + assert_eq!(ProposalCount::<Test>::get(), 1); + // Check invariant 1 holds + assert!(ProposalCount::<Test>::get() as usize >= Proposals::<Test>::iter().count()); + // Break invariant 1 by decreasing `ProposalCount` + ProposalCount::<Test>::put(0); + // Invariant 1 should be violated + assert_eq!( + Treasury::do_try_state(), + Err(Other("Actual number of proposals exceeds `ProposalCount`.")) + ); + }); +} + +#[test] +fn try_state_proposals_invariant_2_works() { + ExtBuilder::default().build().execute_with(|| { + use frame_support::pallet_prelude::DispatchError::Other; + // Add a proposal using `propose_spend` + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 1, 3) + }); + assert_eq!(Proposals::<Test>::iter().count(), 1); + let current_proposal_count = ProposalCount::<Test>::get(); + assert_eq!(current_proposal_count, 1); + // Check invariant 2 holds + assert!( + Proposals::<Test>::iter_keys() + .all(|proposal_index| { + proposal_index < current_proposal_count + }) + ); + // Break invariant 2 by inserting the proposal under key = 1 + let proposal = Proposals::<Test>::take(0).unwrap(); + Proposals::<Test>::insert(1, proposal); + // Invariant 2 should be violated + assert_eq!( + Treasury::do_try_state(), + Err(Other("`ProposalCount` should by strictly greater than any ProposalIndex used as a key for `Proposals`.")) + ); + }); +} + +#[test] +fn try_state_proposals_invariant_3_works() { + ExtBuilder::default().build().execute_with(|| { + use frame_support::pallet_prelude::DispatchError::Other; + // Add a proposal using `propose_spend` + assert_ok!({ + #[allow(deprecated)] + Treasury::propose_spend(RuntimeOrigin::signed(0), 10, 3) + }); + assert_eq!(Proposals::<Test>::iter().count(), 1); + // Approve the proposal + assert_ok!({ + #[allow(deprecated)] + Treasury::approve_proposal(RuntimeOrigin::root(), 0) + }); + assert_eq!(Approvals::<Test>::get().len(), 1); + // Check invariant 3 holds + assert!(Approvals::<Test>::get() + .iter() + .all(|proposal_index| { Proposals::<Test>::contains_key(proposal_index) })); + // Break invariant 3 by adding another key to `Approvals` + let mut approvals_modified = Approvals::<Test>::get(); + approvals_modified.try_push(2).unwrap(); + Approvals::<Test>::put(approvals_modified); + // Invariant 3 should be violated + assert_eq!( + Treasury::do_try_state(), + Err(Other("Proposal indices in `Approvals` must also be contained in `Proposals`.")) + ); + }); +} + +#[test] +fn try_state_spends_invariant_1_works() { + ExtBuilder::default().build().execute_with(|| { + use frame_support::pallet_prelude::DispatchError::Other; + // Propose and approve a spend + assert_ok!({ + Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None) + }); + assert_eq!(Spends::<Test>::iter().count(), 1); + assert_eq!(SpendCount::<Test>::get(), 1); + // Check invariant 1 holds + assert!(SpendCount::<Test>::get() as usize >= Spends::<Test>::iter().count()); + // Break invariant 1 by decreasing `SpendCount` + SpendCount::<Test>::put(0); + // Invariant 1 should be violated + assert_eq!( + Treasury::do_try_state(), + Err(Other("Actual number of spends exceeds `SpendCount`.")) + ); + }); +} + +#[test] +fn try_state_spends_invariant_2_works() { + ExtBuilder::default().build().execute_with(|| { + use frame_support::pallet_prelude::DispatchError::Other; + // Propose and approve a spend + assert_ok!({ + Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None) + }); + assert_eq!(Spends::<Test>::iter().count(), 1); + let current_spend_count = SpendCount::<Test>::get(); + assert_eq!(current_spend_count, 1); + // Check invariant 2 holds + assert!( + Spends::<Test>::iter_keys() + .all(|spend_index| { + spend_index < current_spend_count + }) + ); + // Break invariant 2 by inserting the spend under key = 1 + let spend = Spends::<Test>::take(0).unwrap(); + Spends::<Test>::insert(1, spend); + // Invariant 2 should be violated + assert_eq!( + Treasury::do_try_state(), + Err(Other("`SpendCount` should by strictly greater than any SpendIndex used as a key for `Spends`.")) + ); + }); +} + +#[test] +fn try_state_spends_invariant_3_works() { + ExtBuilder::default().build().execute_with(|| { + use frame_support::pallet_prelude::DispatchError::Other; + // Propose and approve a spend + assert_ok!({ + Treasury::spend(RuntimeOrigin::signed(10), Box::new(1), 1, Box::new(6), None) + }); + assert_eq!(Spends::<Test>::iter().count(), 1); + let current_spend_count = SpendCount::<Test>::get(); + assert_eq!(current_spend_count, 1); + // Check invariant 3 holds + assert!(Spends::<Test>::iter_values() + .all(|SpendStatus { valid_from, expire_at, .. }| { valid_from < expire_at })); + // Break invariant 3 by reversing spend.expire_at and spend.valid_from + let spend = Spends::<Test>::take(0).unwrap(); + Spends::<Test>::insert( + 0, + SpendStatus { valid_from: spend.expire_at, expire_at: spend.valid_from, ..spend }, + ); + // Invariant 3 should be violated + assert_eq!( + Treasury::do_try_state(), + Err(Other("Spend cannot expire before it becomes valid.")) + ); + }); +} From 7aace06b3d653529ab992c8ff96571d54bf00a36 Mon Sep 17 00:00:00 2001 From: Tsvetomir Dimitrov <tsvetomir@parity.io> Date: Thu, 12 Oct 2023 16:01:07 +0300 Subject: [PATCH 36/54] Disabled validators runtime API (#1257) Exposes disabled validators list via a runtime API. --------- Co-authored-by: ordian <noreply@reusable.software> Co-authored-by: ordian <write@reusable.software> --- .../src/blockchain_rpc_client.rs | 7 +++++ .../src/rpc_client.rs | 8 ++++++ .../emulated/common/src/lib.rs | 6 ++--- polkadot/node/core/runtime-api/src/cache.rs | 18 +++++++++++++ polkadot/node/core/runtime-api/src/lib.rs | 10 +++++++ polkadot/node/core/runtime-api/src/tests.rs | 4 +++ polkadot/node/subsystem-types/src/messages.rs | 5 ++++ .../subsystem-types/src/runtime_client.rs | 9 ++++++- polkadot/node/subsystem-util/src/lib.rs | 1 + polkadot/primitives/src/runtime_api.rs | 7 +++++ .../src/runtime_api_impl/vstaging.rs | 27 +++++++++++++++++++ polkadot/runtime/rococo/src/lib.rs | 11 ++++++-- polkadot/runtime/westend/src/lib.rs | 10 +++++-- 13 files changed, 115 insertions(+), 8 deletions(-) diff --git a/cumulus/client/relay-chain-minimal-node/src/blockchain_rpc_client.rs b/cumulus/client/relay-chain-minimal-node/src/blockchain_rpc_client.rs index 3f4c08ecbb834..1e78df7115435 100644 --- a/cumulus/client/relay-chain-minimal-node/src/blockchain_rpc_client.rs +++ b/cumulus/client/relay-chain-minimal-node/src/blockchain_rpc_client.rs @@ -346,6 +346,13 @@ impl RuntimeApiSubsystemClient for BlockChainRpcClient { Ok(self.rpc_client.parachain_host_minimum_backing_votes(at, session_index).await?) } + async fn disabled_validators( + &self, + at: Hash, + ) -> Result<Vec<polkadot_primitives::ValidatorIndex>, ApiError> { + Ok(self.rpc_client.parachain_host_disabled_validators(at).await?) + } + async fn async_backing_params(&self, at: Hash) -> Result<AsyncBackingParams, ApiError> { Ok(self.rpc_client.parachain_host_async_backing_params(at).await?) } diff --git a/cumulus/client/relay-chain-rpc-interface/src/rpc_client.rs b/cumulus/client/relay-chain-rpc-interface/src/rpc_client.rs index b1fd7d1ab7d9c..5924716adcb41 100644 --- a/cumulus/client/relay-chain-rpc-interface/src/rpc_client.rs +++ b/cumulus/client/relay-chain-rpc-interface/src/rpc_client.rs @@ -597,6 +597,14 @@ impl RelayChainRpcClient { .await } + pub async fn parachain_host_disabled_validators( + &self, + at: RelayHash, + ) -> Result<Vec<ValidatorIndex>, RelayChainError> { + self.call_remote_runtime_function("ParachainHost_disabled_validators", at, None::<()>) + .await + } + #[allow(missing_docs)] pub async fn parachain_host_async_backing_params( &self, diff --git a/cumulus/parachains/integration-tests/emulated/common/src/lib.rs b/cumulus/parachains/integration-tests/emulated/common/src/lib.rs index c2e065ccadcdf..f8fe8831d3c74 100644 --- a/cumulus/parachains/integration-tests/emulated/common/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/common/src/lib.rs @@ -33,7 +33,7 @@ use xcm_emulator::{ }; decl_test_relay_chains! { - #[api_version(7)] + #[api_version(8)] pub struct Westend { genesis = westend::genesis(), on_init = (), @@ -50,7 +50,7 @@ decl_test_relay_chains! { AssetRate: westend_runtime::AssetRate, } }, - #[api_version(7)] + #[api_version(8)] pub struct Rococo { genesis = rococo::genesis(), on_init = (), @@ -65,7 +65,7 @@ decl_test_relay_chains! { Balances: rococo_runtime::Balances, } }, - #[api_version(7)] + #[api_version(8)] pub struct Wococo { genesis = rococo::genesis(), on_init = (), diff --git a/polkadot/node/core/runtime-api/src/cache.rs b/polkadot/node/core/runtime-api/src/cache.rs index 6cf7fa744d3f9..69eea22b23bda 100644 --- a/polkadot/node/core/runtime-api/src/cache.rs +++ b/polkadot/node/core/runtime-api/src/cache.rs @@ -64,6 +64,7 @@ pub(crate) struct RequestResultCache { unapplied_slashes: LruMap<Hash, Vec<(SessionIndex, CandidateHash, slashing::PendingSlashes)>>, key_ownership_proof: LruMap<(Hash, ValidatorId), Option<slashing::OpaqueKeyOwnershipProof>>, minimum_backing_votes: LruMap<SessionIndex, u32>, + disabled_validators: LruMap<Hash, Vec<ValidatorIndex>>, para_backing_state: LruMap<(Hash, ParaId), Option<async_backing::BackingState>>, async_backing_params: LruMap<Hash, async_backing::AsyncBackingParams>, } @@ -96,6 +97,7 @@ impl Default for RequestResultCache { unapplied_slashes: LruMap::new(ByLength::new(DEFAULT_CACHE_CAP)), key_ownership_proof: LruMap::new(ByLength::new(DEFAULT_CACHE_CAP)), minimum_backing_votes: LruMap::new(ByLength::new(DEFAULT_CACHE_CAP)), + disabled_validators: LruMap::new(ByLength::new(DEFAULT_CACHE_CAP)), para_backing_state: LruMap::new(ByLength::new(DEFAULT_CACHE_CAP)), async_backing_params: LruMap::new(ByLength::new(DEFAULT_CACHE_CAP)), } @@ -444,6 +446,21 @@ impl RequestResultCache { self.minimum_backing_votes.insert(session_index, minimum_backing_votes); } + pub(crate) fn disabled_validators( + &mut self, + relay_parent: &Hash, + ) -> Option<&Vec<ValidatorIndex>> { + self.disabled_validators.get(relay_parent).map(|v| &*v) + } + + pub(crate) fn cache_disabled_validators( + &mut self, + relay_parent: Hash, + disabled_validators: Vec<ValidatorIndex>, + ) { + self.disabled_validators.insert(relay_parent, disabled_validators); + } + pub(crate) fn para_backing_state( &mut self, key: (Hash, ParaId), @@ -520,6 +537,7 @@ pub(crate) enum RequestResult { slashing::OpaqueKeyOwnershipProof, Option<()>, ), + DisabledValidators(Hash, Vec<ValidatorIndex>), ParaBackingState(Hash, ParaId, Option<async_backing::BackingState>), AsyncBackingParams(Hash, async_backing::AsyncBackingParams), } diff --git a/polkadot/node/core/runtime-api/src/lib.rs b/polkadot/node/core/runtime-api/src/lib.rs index 1b18941e54645..bdcca08b10dda 100644 --- a/polkadot/node/core/runtime-api/src/lib.rs +++ b/polkadot/node/core/runtime-api/src/lib.rs @@ -166,6 +166,8 @@ where .requests_cache .cache_key_ownership_proof((relay_parent, validator_id), key_ownership_proof), SubmitReportDisputeLost(_, _, _, _) => {}, + DisabledValidators(relay_parent, disabled_validators) => + self.requests_cache.cache_disabled_validators(relay_parent, disabled_validators), ParaBackingState(relay_parent, para_id, constraints) => self .requests_cache .cache_para_backing_state((relay_parent, para_id), constraints), @@ -296,6 +298,8 @@ where Request::SubmitReportDisputeLost(dispute_proof, key_ownership_proof, sender) }, ), + Request::DisabledValidators(sender) => query!(disabled_validators(), sender) + .map(|sender| Request::DisabledValidators(sender)), Request::ParaBackingState(para, sender) => query!(para_backing_state(para), sender) .map(|sender| Request::ParaBackingState(para, sender)), Request::AsyncBackingParams(sender) => query!(async_backing_params(), sender) @@ -565,6 +569,12 @@ where ver = Request::MINIMUM_BACKING_VOTES_RUNTIME_REQUIREMENT, sender ), + Request::DisabledValidators(sender) => query!( + DisabledValidators, + disabled_validators(), + ver = Request::DISABLED_VALIDATORS_RUNTIME_REQUIREMENT, + sender + ), Request::ParaBackingState(para, sender) => { query!( ParaBackingState, diff --git a/polkadot/node/core/runtime-api/src/tests.rs b/polkadot/node/core/runtime-api/src/tests.rs index fb97139a80287..979b3587d2692 100644 --- a/polkadot/node/core/runtime-api/src/tests.rs +++ b/polkadot/node/core/runtime-api/src/tests.rs @@ -268,6 +268,10 @@ impl RuntimeApiSubsystemClient for MockSubsystemClient { async fn minimum_backing_votes(&self, _: Hash, _: SessionIndex) -> Result<u32, ApiError> { todo!("Not required for tests") } + + async fn disabled_validators(&self, _: Hash) -> Result<Vec<ValidatorIndex>, ApiError> { + todo!("Not required for tests") + } } #[test] diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index 01ccee3add904..6bc874384594e 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -695,6 +695,8 @@ pub enum RuntimeApiRequest { ), /// Get the minimum required backing votes. MinimumBackingVotes(SessionIndex, RuntimeApiSender<u32>), + /// Returns all disabled validators at a given block height. + DisabledValidators(RuntimeApiSender<Vec<ValidatorIndex>>), /// Get the backing state of the given para. ParaBackingState(ParaId, RuntimeApiSender<Option<async_backing::BackingState>>), /// Get candidate's acceptance limitations for asynchronous backing for a relay parent. @@ -726,6 +728,9 @@ impl RuntimeApiRequest { /// Minimum version to enable asynchronous backing: `AsyncBackingParams` and `ParaBackingState`. pub const ASYNC_BACKING_STATE_RUNTIME_REQUIREMENT: u32 = 7; + + /// `DisabledValidators` + pub const DISABLED_VALIDATORS_RUNTIME_REQUIREMENT: u32 = 8; } /// A message to the Runtime API subsystem. diff --git a/polkadot/node/subsystem-types/src/runtime_client.rs b/polkadot/node/subsystem-types/src/runtime_client.rs index 3007e985b4f7b..f7adcf9862b5d 100644 --- a/polkadot/node/subsystem-types/src/runtime_client.rs +++ b/polkadot/node/subsystem-types/src/runtime_client.rs @@ -255,6 +255,10 @@ pub trait RuntimeApiSubsystemClient { at: Hash, para_id: Id, ) -> Result<Option<async_backing::BackingState>, ApiError>; + + // === v8 === + /// Gets the disabled validators at a specific block height + async fn disabled_validators(&self, at: Hash) -> Result<Vec<ValidatorIndex>, ApiError>; } /// Default implementation of [`RuntimeApiSubsystemClient`] using the client. @@ -497,11 +501,14 @@ where self.client.runtime_api().para_backing_state(at, para_id) } - /// Returns candidate's acceptance limitations for asynchronous backing for a relay parent. async fn async_backing_params( &self, at: Hash, ) -> Result<async_backing::AsyncBackingParams, ApiError> { self.client.runtime_api().async_backing_params(at) } + + async fn disabled_validators(&self, at: Hash) -> Result<Vec<ValidatorIndex>, ApiError> { + self.client.runtime_api().disabled_validators(at) + } } diff --git a/polkadot/node/subsystem-util/src/lib.rs b/polkadot/node/subsystem-util/src/lib.rs index 57e4f9cde09af..a5f3e9d4a0c0e 100644 --- a/polkadot/node/subsystem-util/src/lib.rs +++ b/polkadot/node/subsystem-util/src/lib.rs @@ -226,6 +226,7 @@ specialize_requests! { fn request_unapplied_slashes() -> Vec<(SessionIndex, CandidateHash, slashing::PendingSlashes)>; UnappliedSlashes; fn request_key_ownership_proof(validator_id: ValidatorId) -> Option<slashing::OpaqueKeyOwnershipProof>; KeyOwnershipProof; fn request_submit_report_dispute_lost(dp: slashing::DisputeProof, okop: slashing::OpaqueKeyOwnershipProof) -> Option<()>; SubmitReportDisputeLost; + fn request_disabled_validators() -> Vec<ValidatorIndex>; DisabledValidators; fn request_async_backing_params() -> AsyncBackingParams; AsyncBackingParams; } diff --git a/polkadot/primitives/src/runtime_api.rs b/polkadot/primitives/src/runtime_api.rs index 6cb66d40204d1..5ec897c8cbb40 100644 --- a/polkadot/primitives/src/runtime_api.rs +++ b/polkadot/primitives/src/runtime_api.rs @@ -248,6 +248,7 @@ sp_api::decl_runtime_apis! { #[api_version(6)] fn minimum_backing_votes() -> u32; + /***** Added in v7: Asynchronous backing *****/ /// Returns the state of parachain backing for a given para. @@ -257,5 +258,11 @@ sp_api::decl_runtime_apis! { /// Returns candidate's acceptance limitations for asynchronous backing for a relay parent. #[api_version(7)] fn async_backing_params() -> AsyncBackingParams; + + /***** Added in v8 *****/ + + /// Returns a list of all disabled validators at the given block. + #[api_version(8)] + fn disabled_validators() -> Vec<ValidatorIndex>; } } diff --git a/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs b/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs index d01b543630c31..24a076f3a4431 100644 --- a/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs +++ b/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs @@ -15,3 +15,30 @@ // along with Polkadot. If not, see <http://www.gnu.org/licenses/>. //! Put implementations of functions from staging APIs here. + +use crate::shared; +use primitives::ValidatorIndex; +use sp_std::{collections::btree_map::BTreeMap, prelude::Vec}; + +/// Implementation for `DisabledValidators` +// CAVEAT: this should only be called on the node side +// as it might produce incorrect results on session boundaries +pub fn disabled_validators<T>() -> Vec<ValidatorIndex> +where + T: pallet_session::Config + shared::Config, +{ + let shuffled_indices = <shared::Pallet<T>>::active_validator_indices(); + // mapping from raw validator index to `ValidatorIndex` + // this computation is the same within a session, but should be cheap + let reverse_index = shuffled_indices + .iter() + .enumerate() + .map(|(i, v)| (v.0, ValidatorIndex(i as u32))) + .collect::<BTreeMap<u32, ValidatorIndex>>(); + + // we might have disabled validators who are not parachain validators + <pallet_session::Pallet<T>>::disabled_validators() + .iter() + .filter_map(|v| reverse_index.get(v).cloned()) + .collect() +} diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index fd3e656f69573..9933f64429745 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -49,7 +49,9 @@ use runtime_parachains::{ inclusion::{AggregateMessageOrigin, UmpQueueId}, initializer as parachains_initializer, origin as parachains_origin, paras as parachains_paras, paras_inherent as parachains_paras_inherent, - runtime_api_impl::v7 as parachains_runtime_api_impl, + runtime_api_impl::{ + v7 as parachains_runtime_api_impl, vstaging as parachains_staging_runtime_api_impl, + }, scheduler as parachains_scheduler, session_info as parachains_session_info, shared as parachains_shared, }; @@ -1585,7 +1587,7 @@ sp_api::impl_runtime_apis! { } } - #[api_version(7)] + #[api_version(8)] impl primitives::runtime_api::ParachainHost<Block, Hash, BlockNumber> for Runtime { fn validators() -> Vec<ValidatorId> { parachains_runtime_api_impl::validators::<Runtime>() @@ -1728,6 +1730,11 @@ sp_api::impl_runtime_apis! { fn async_backing_params() -> primitives::AsyncBackingParams { parachains_runtime_api_impl::async_backing_params::<Runtime>() } + + fn disabled_validators() -> Vec<ValidatorIndex> { + parachains_staging_runtime_api_impl::disabled_validators::<Runtime>() + } + } #[api_version(3)] diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index d61acf36b4cb8..0e93b3449fec5 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -70,7 +70,9 @@ use runtime_parachains::{ inclusion::{AggregateMessageOrigin, UmpQueueId}, initializer as parachains_initializer, origin as parachains_origin, paras as parachains_paras, paras_inherent as parachains_paras_inherent, reward_points as parachains_reward_points, - runtime_api_impl::v7 as parachains_runtime_api_impl, + runtime_api_impl::{ + v7 as parachains_runtime_api_impl, vstaging as parachains_staging_runtime_api_impl, + }, scheduler as parachains_scheduler, session_info as parachains_session_info, shared as parachains_shared, }; @@ -1695,7 +1697,7 @@ sp_api::impl_runtime_apis! { } } - #[api_version(7)] + #[api_version(8)] impl primitives::runtime_api::ParachainHost<Block, Hash, BlockNumber> for Runtime { fn validators() -> Vec<ValidatorId> { parachains_runtime_api_impl::validators::<Runtime>() @@ -1838,6 +1840,10 @@ sp_api::impl_runtime_apis! { fn async_backing_params() -> primitives::AsyncBackingParams { parachains_runtime_api_impl::async_backing_params::<Runtime>() } + + fn disabled_validators() -> Vec<ValidatorIndex> { + parachains_staging_runtime_api_impl::disabled_validators::<Runtime>() + } } impl beefy_primitives::BeefyApi<Block, BeefyId> for Runtime { From d2fc1d7c91971e6e630a9db8cb627f8fdc91e8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Vilhelm=20=C3=81sgeirsson?= <antonva@users.noreply.github.com> Date: Thu, 12 Oct 2023 21:29:10 +0200 Subject: [PATCH 37/54] Fix links to implementers' guide (#1865) # Description In a couple of cases, there were links pointing to the w3f github pages domain. In other instances, there were links pointing to the old polkadot repo's github pages. Both of these are now pointing to the relevant links in https://paritytech.github.io/polkadot-sdk/book/index.html. These changes were made specifically because the w3f github pages returns a 404, and while fixing the links, the old polkadot repo links were touched up as well even if they do redirect properly. This shouldn't affect anything as these are documentation link changes only. --- polkadot/node/collation-generation/src/lib.rs | 2 +- polkadot/node/core/pvf/src/lib.rs | 6 ++++-- polkadot/node/network/approval-distribution/src/lib.rs | 5 ++++- polkadot/node/overseer/src/lib.rs | 4 +++- polkadot/node/service/src/relay_chain_selection.rs | 2 +- polkadot/roadmap/implementers-guide/README.md | 2 +- polkadot/runtime/parachains/src/session_info.rs | 5 +++-- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/polkadot/node/collation-generation/src/lib.rs b/polkadot/node/collation-generation/src/lib.rs index 4e13755deedfb..b8c9c1a36e46b 100644 --- a/polkadot/node/collation-generation/src/lib.rs +++ b/polkadot/node/collation-generation/src/lib.rs @@ -193,7 +193,7 @@ async fn handle_new_activations<Context>( metrics: Metrics, ) -> crate::error::Result<()> { // follow the procedure from the guide: - // https://paritytech.github.io/polkadot/book/node/collators/collation-generation.html + // https://paritytech.github.io/polkadot-sdk/book/node/collators/collation-generation.html if config.collator.is_none() { return Ok(()) diff --git a/polkadot/node/core/pvf/src/lib.rs b/polkadot/node/core/pvf/src/lib.rs index 1b8d83773813a..e3c6da9c4c6ba 100644 --- a/polkadot/node/core/pvf/src/lib.rs +++ b/polkadot/node/core/pvf/src/lib.rs @@ -19,8 +19,10 @@ //! The PVF validation host. Responsible for coordinating preparation and execution of PVFs. //! //! For more background, refer to the Implementer's Guide: [PVF -//! Pre-checking](https://paritytech.github.io/polkadot/book/pvf-prechecking.html) and [Candidate -//! Validation](https://paritytech.github.io/polkadot/book/node/utility/candidate-validation.html#pvf-host). +//! Pre-checking](https://paritytech.github.io/polkadot-sdk/book/pvf-prechecking.html), [Candidate +//! Validation](https://paritytech.github.io/polkadot-sdk/book/node/utility/candidate-validation.html) +//! and [PVF Host and Workers](https://paritytech.github.io/polkadot-sdk/book/node/utility/pvf-host-and-workers.html). +//! //! //! # Entrypoint //! diff --git a/polkadot/node/network/approval-distribution/src/lib.rs b/polkadot/node/network/approval-distribution/src/lib.rs index f76826d7fdf42..d8949b1c1123c 100644 --- a/polkadot/node/network/approval-distribution/src/lib.rs +++ b/polkadot/node/network/approval-distribution/src/lib.rs @@ -16,7 +16,10 @@ //! [`ApprovalDistribution`] implementation. //! -//! <https://w3f.github.io/parachain-implementers-guide/node/approval/approval-distribution.html> +//! See the documentation on [approval distribution][approval-distribution-page] in the +//! implementers' guide. +//! +//! [approval-distribution-page]: https://paritytech.github.io/polkadot-sdk/book/node/approval/approval-distribution.html #![warn(missing_docs)] diff --git a/polkadot/node/overseer/src/lib.rs b/polkadot/node/overseer/src/lib.rs index 84d5d19c3b93c..673569ab946f0 100644 --- a/polkadot/node/overseer/src/lib.rs +++ b/polkadot/node/overseer/src/lib.rs @@ -17,7 +17,7 @@ //! # Overseer //! //! `overseer` implements the Overseer architecture described in the -//! [implementers-guide](https://w3f.github.io/parachain-implementers-guide/node/index.html). +//! [implementers' guide][overseer-page]. //! For the motivations behind implementing the overseer itself you should //! check out that guide, documentation in this crate will be mostly discussing //! technical stuff. @@ -53,6 +53,8 @@ //! . +--------------------+ +---------------------+ . //! .................................................................. //! ``` +//! +//! [overseer-page]: https://paritytech.github.io/polkadot-sdk/book/node/overseer.html // #![deny(unused_results)] // unused dependencies can not work for test and examples at the same time diff --git a/polkadot/node/service/src/relay_chain_selection.rs b/polkadot/node/service/src/relay_chain_selection.rs index 189073783f0dc..5fae6a96de4ea 100644 --- a/polkadot/node/service/src/relay_chain_selection.rs +++ b/polkadot/node/service/src/relay_chain_selection.rs @@ -31,7 +31,7 @@ //! leaf returned from the chain selection subsystem by calling into other //! subsystems which yield information about approvals and disputes. //! -//! [chain-selection-guide]: https://w3f.github.io/parachain-implementers-guide/protocol-chain-selection.html +//! [chain-selection-guide]: https://paritytech.github.io/polkadot-sdk/book/protocol-chain-selection.html #![cfg(feature = "full-node")] diff --git a/polkadot/roadmap/implementers-guide/README.md b/polkadot/roadmap/implementers-guide/README.md index 996041f176bb2..e03c0c45ddba0 100644 --- a/polkadot/roadmap/implementers-guide/README.md +++ b/polkadot/roadmap/implementers-guide/README.md @@ -4,7 +4,7 @@ The implementers' guide is compiled from several source files with [`mdBook`](ht ## Hosted build -This is available [here](https://paritytech.github.io/polkadot/book/). +This is available [here](https://paritytech.github.io/polkadot-sdk/book/). ## Local build diff --git a/polkadot/runtime/parachains/src/session_info.rs b/polkadot/runtime/parachains/src/session_info.rs index 0043851be0f22..9e1b3d05842fd 100644 --- a/polkadot/runtime/parachains/src/session_info.rs +++ b/polkadot/runtime/parachains/src/session_info.rs @@ -17,8 +17,9 @@ //! The session info pallet provides information about validator sets //! from prior sessions needed for approvals and disputes. //! -//! See <https://w3f.github.io/parachain-implementers-guide/runtime/session_info.html>. - +//! See the documentation on [session info][session-info-page] in the implementers' guide. +//! +//! [session-info-page]: https://paritytech.github.io/polkadot-sdk/book/runtime/session_info.html use crate::{ configuration, paras, scheduler, shared, util::{take_active_subset, take_active_subset_and_inactive}, From 6b27dad359793a873c91d09cd3f6267d66ff543e Mon Sep 17 00:00:00 2001 From: gupnik <17176722+gupnik@users.noreply.github.com> Date: Fri, 13 Oct 2023 07:48:51 +0200 Subject: [PATCH 38/54] Adds instance support for composite enums (#1857) Fixes https://github.com/paritytech/polkadot-sdk/issues/1839 Currently, `composite_enum`s do not support pallet instances. This PR allows the following: ```rust #[pallet::composite_enum] pub enum HoldReason<I: 'static = ()> { SomeHoldReason } ``` ### Todo - [x] UI Test --- substrate/frame/balances/src/lib.rs | 4 +- .../expand/composite_helper.rs | 70 ++++++++++++++++ .../construct_runtime/expand/freeze_reason.rs | 40 +++++----- .../construct_runtime/expand/hold_reason.rs | 40 +++++----- .../src/construct_runtime/expand/lock_id.rs | 40 +++++----- .../src/construct_runtime/expand/mod.rs | 1 + .../construct_runtime/expand/slash_reason.rs | 40 +++++----- .../procedural/src/pallet/parse/composite.rs | 24 +++++- substrate/frame/support/src/lib.rs | 17 +++- .../test/tests/construct_runtime_ui.rs | 1 + .../pass/composite_enum_instance.rs | 80 +++++++++++++++++++ 11 files changed, 264 insertions(+), 93 deletions(-) create mode 100644 substrate/frame/support/procedural/src/construct_runtime/expand/composite_helper.rs create mode 100644 substrate/frame/support/test/tests/construct_runtime_ui/pass/composite_enum_instance.rs diff --git a/substrate/frame/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index a2cacc45369a2..c408bf3f35f3b 100644 --- a/substrate/frame/balances/src/lib.rs +++ b/substrate/frame/balances/src/lib.rs @@ -256,7 +256,7 @@ pub mod pallet { /// The overarching hold reason. #[pallet::no_default_bounds] - type RuntimeHoldReason: Parameter + Member + MaxEncodedLen + Ord + Copy; + type RuntimeHoldReason: Parameter + Member + MaxEncodedLen + Copy; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; @@ -300,7 +300,7 @@ pub mod pallet { type ReserveIdentifier: Parameter + Member + MaxEncodedLen + Ord + Copy; /// The ID type for freezes. - type FreezeIdentifier: Parameter + Member + MaxEncodedLen + Ord + Copy; + type FreezeIdentifier: Parameter + Member + MaxEncodedLen + Copy; /// The maximum number of locks that should exist on an account. /// Not strictly enforced, but used for weight estimation. diff --git a/substrate/frame/support/procedural/src/construct_runtime/expand/composite_helper.rs b/substrate/frame/support/procedural/src/construct_runtime/expand/composite_helper.rs new file mode 100644 index 0000000000000..3c81d2360cb7b --- /dev/null +++ b/substrate/frame/support/procedural/src/construct_runtime/expand/composite_helper.rs @@ -0,0 +1,70 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// 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 crate::construct_runtime::parse::PalletPath; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; + +pub(crate) fn expand_conversion_fn( + composite_name: &str, + path: &PalletPath, + instance: Option<&Ident>, + variant_name: &Ident, +) -> TokenStream { + let composite_name = quote::format_ident!("{}", composite_name); + let runtime_composite_name = quote::format_ident!("Runtime{}", composite_name); + + if let Some(inst) = instance { + quote! { + impl From<#path::#composite_name<#path::#inst>> for #runtime_composite_name { + fn from(hr: #path::#composite_name<#path::#inst>) -> Self { + #runtime_composite_name::#variant_name(hr) + } + } + } + } else { + quote! { + impl From<#path::#composite_name> for #runtime_composite_name { + fn from(hr: #path::#composite_name) -> Self { + #runtime_composite_name::#variant_name(hr) + } + } + } + } +} + +pub(crate) fn expand_variant( + composite_name: &str, + index: u8, + path: &PalletPath, + instance: Option<&Ident>, + variant_name: &Ident, +) -> TokenStream { + let composite_name = quote::format_ident!("{}", composite_name); + + if let Some(inst) = instance { + quote! { + #[codec(index = #index)] + #variant_name(#path::#composite_name<#path::#inst>), + } + } else { + quote! { + #[codec(index = #index)] + #variant_name(#path::#composite_name), + } + } +} diff --git a/substrate/frame/support/procedural/src/construct_runtime/expand/freeze_reason.rs b/substrate/frame/support/procedural/src/construct_runtime/expand/freeze_reason.rs index b142f8e84c92f..18790850d6b3c 100644 --- a/substrate/frame/support/procedural/src/construct_runtime/expand/freeze_reason.rs +++ b/substrate/frame/support/procedural/src/construct_runtime/expand/freeze_reason.rs @@ -15,8 +15,9 @@ // See the License for the specific language governing permissions and // limitations under the License -use crate::construct_runtime::{parse::PalletPath, Pallet}; -use proc_macro2::{Ident, TokenStream}; +use super::composite_helper; +use crate::construct_runtime::Pallet; +use proc_macro2::TokenStream; use quote::quote; pub fn expand_outer_freeze_reason(pallet_decls: &[Pallet], scrate: &TokenStream) -> TokenStream { @@ -27,17 +28,29 @@ pub fn expand_outer_freeze_reason(pallet_decls: &[Pallet], scrate: &TokenStream) let variant_name = &decl.name; let path = &decl.path; let index = decl.index; + let instance = decl.instance.as_ref(); - conversion_fns.push(expand_conversion_fn(path, variant_name)); + conversion_fns.push(composite_helper::expand_conversion_fn( + "FreezeReason", + path, + instance, + variant_name, + )); - freeze_reason_variants.push(expand_variant(index, path, variant_name)); + freeze_reason_variants.push(composite_helper::expand_variant( + "FreezeReason", + index, + path, + instance, + variant_name, + )); } } quote! { /// A reason for placing a freeze on funds. #[derive( - Copy, Clone, Eq, PartialEq, Ord, PartialOrd, + Copy, Clone, Eq, PartialEq, #scrate::__private::codec::Encode, #scrate::__private::codec::Decode, #scrate::__private::codec::MaxEncodedLen, #scrate::__private::scale_info::TypeInfo, #scrate::__private::RuntimeDebug, @@ -49,20 +62,3 @@ pub fn expand_outer_freeze_reason(pallet_decls: &[Pallet], scrate: &TokenStream) #( #conversion_fns )* } } - -fn expand_conversion_fn(path: &PalletPath, variant_name: &Ident) -> TokenStream { - quote! { - impl From<#path::FreezeReason> for RuntimeFreezeReason { - fn from(hr: #path::FreezeReason) -> Self { - RuntimeFreezeReason::#variant_name(hr) - } - } - } -} - -fn expand_variant(index: u8, path: &PalletPath, variant_name: &Ident) -> TokenStream { - quote! { - #[codec(index = #index)] - #variant_name(#path::FreezeReason), - } -} diff --git a/substrate/frame/support/procedural/src/construct_runtime/expand/hold_reason.rs b/substrate/frame/support/procedural/src/construct_runtime/expand/hold_reason.rs index ed7183c4a150d..1bb7462133cdd 100644 --- a/substrate/frame/support/procedural/src/construct_runtime/expand/hold_reason.rs +++ b/substrate/frame/support/procedural/src/construct_runtime/expand/hold_reason.rs @@ -15,8 +15,9 @@ // See the License for the specific language governing permissions and // limitations under the License -use crate::construct_runtime::{parse::PalletPath, Pallet}; -use proc_macro2::{Ident, TokenStream}; +use super::composite_helper; +use crate::construct_runtime::Pallet; +use proc_macro2::TokenStream; use quote::quote; pub fn expand_outer_hold_reason(pallet_decls: &[Pallet], scrate: &TokenStream) -> TokenStream { @@ -27,17 +28,29 @@ pub fn expand_outer_hold_reason(pallet_decls: &[Pallet], scrate: &TokenStream) - let variant_name = &decl.name; let path = &decl.path; let index = decl.index; + let instance = decl.instance.as_ref(); - conversion_fns.push(expand_conversion_fn(path, variant_name)); + conversion_fns.push(composite_helper::expand_conversion_fn( + "HoldReason", + path, + instance, + variant_name, + )); - hold_reason_variants.push(expand_variant(index, path, variant_name)); + hold_reason_variants.push(composite_helper::expand_variant( + "HoldReason", + index, + path, + instance, + variant_name, + )); } } quote! { /// A reason for placing a hold on funds. #[derive( - Copy, Clone, Eq, PartialEq, Ord, PartialOrd, + Copy, Clone, Eq, PartialEq, #scrate::__private::codec::Encode, #scrate::__private::codec::Decode, #scrate::__private::codec::MaxEncodedLen, #scrate::__private::scale_info::TypeInfo, #scrate::__private::RuntimeDebug, @@ -49,20 +62,3 @@ pub fn expand_outer_hold_reason(pallet_decls: &[Pallet], scrate: &TokenStream) - #( #conversion_fns )* } } - -fn expand_conversion_fn(path: &PalletPath, variant_name: &Ident) -> TokenStream { - quote! { - impl From<#path::HoldReason> for RuntimeHoldReason { - fn from(hr: #path::HoldReason) -> Self { - RuntimeHoldReason::#variant_name(hr) - } - } - } -} - -fn expand_variant(index: u8, path: &PalletPath, variant_name: &Ident) -> TokenStream { - quote! { - #[codec(index = #index)] - #variant_name(#path::HoldReason), - } -} diff --git a/substrate/frame/support/procedural/src/construct_runtime/expand/lock_id.rs b/substrate/frame/support/procedural/src/construct_runtime/expand/lock_id.rs index ba35147a051fb..e67c0da00ea15 100644 --- a/substrate/frame/support/procedural/src/construct_runtime/expand/lock_id.rs +++ b/substrate/frame/support/procedural/src/construct_runtime/expand/lock_id.rs @@ -15,8 +15,9 @@ // See the License for the specific language governing permissions and // limitations under the License -use crate::construct_runtime::{parse::PalletPath, Pallet}; -use proc_macro2::{Ident, TokenStream}; +use super::composite_helper; +use crate::construct_runtime::Pallet; +use proc_macro2::TokenStream; use quote::quote; pub fn expand_outer_lock_id(pallet_decls: &[Pallet], scrate: &TokenStream) -> TokenStream { @@ -27,17 +28,29 @@ pub fn expand_outer_lock_id(pallet_decls: &[Pallet], scrate: &TokenStream) -> To let variant_name = &decl.name; let path = &decl.path; let index = decl.index; + let instance = decl.instance.as_ref(); - conversion_fns.push(expand_conversion_fn(path, variant_name)); + conversion_fns.push(composite_helper::expand_conversion_fn( + "LockId", + path, + instance, + variant_name, + )); - lock_id_variants.push(expand_variant(index, path, variant_name)); + lock_id_variants.push(composite_helper::expand_variant( + "LockId", + index, + path, + instance, + variant_name, + )); } } quote! { /// An identifier for each lock placed on funds. #[derive( - Copy, Clone, Eq, PartialEq, Ord, PartialOrd, + Copy, Clone, Eq, PartialEq, #scrate::__private::codec::Encode, #scrate::__private::codec::Decode, #scrate::__private::codec::MaxEncodedLen, #scrate::__private::scale_info::TypeInfo, #scrate::__private::RuntimeDebug, @@ -49,20 +62,3 @@ pub fn expand_outer_lock_id(pallet_decls: &[Pallet], scrate: &TokenStream) -> To #( #conversion_fns )* } } - -fn expand_conversion_fn(path: &PalletPath, variant_name: &Ident) -> TokenStream { - quote! { - impl From<#path::LockId> for RuntimeLockId { - fn from(hr: #path::LockId) -> Self { - RuntimeLockId::#variant_name(hr) - } - } - } -} - -fn expand_variant(index: u8, path: &PalletPath, variant_name: &Ident) -> TokenStream { - quote! { - #[codec(index = #index)] - #variant_name(#path::LockId), - } -} diff --git a/substrate/frame/support/procedural/src/construct_runtime/expand/mod.rs b/substrate/frame/support/procedural/src/construct_runtime/expand/mod.rs index 830338f9265ff..a0fc6b8130b3c 100644 --- a/substrate/frame/support/procedural/src/construct_runtime/expand/mod.rs +++ b/substrate/frame/support/procedural/src/construct_runtime/expand/mod.rs @@ -16,6 +16,7 @@ // limitations under the License mod call; +pub mod composite_helper; mod config; mod freeze_reason; mod hold_reason; diff --git a/substrate/frame/support/procedural/src/construct_runtime/expand/slash_reason.rs b/substrate/frame/support/procedural/src/construct_runtime/expand/slash_reason.rs index 2a3283230ad84..892b842b17487 100644 --- a/substrate/frame/support/procedural/src/construct_runtime/expand/slash_reason.rs +++ b/substrate/frame/support/procedural/src/construct_runtime/expand/slash_reason.rs @@ -15,8 +15,9 @@ // See the License for the specific language governing permissions and // limitations under the License -use crate::construct_runtime::{parse::PalletPath, Pallet}; -use proc_macro2::{Ident, TokenStream}; +use super::composite_helper; +use crate::construct_runtime::Pallet; +use proc_macro2::TokenStream; use quote::quote; pub fn expand_outer_slash_reason(pallet_decls: &[Pallet], scrate: &TokenStream) -> TokenStream { @@ -27,17 +28,29 @@ pub fn expand_outer_slash_reason(pallet_decls: &[Pallet], scrate: &TokenStream) let variant_name = &decl.name; let path = &decl.path; let index = decl.index; + let instance = decl.instance.as_ref(); - conversion_fns.push(expand_conversion_fn(path, variant_name)); + conversion_fns.push(composite_helper::expand_conversion_fn( + "SlashReason", + path, + instance, + variant_name, + )); - slash_reason_variants.push(expand_variant(index, path, variant_name)); + slash_reason_variants.push(composite_helper::expand_variant( + "SlashReason", + index, + path, + instance, + variant_name, + )); } } quote! { /// A reason for slashing funds. #[derive( - Copy, Clone, Eq, PartialEq, Ord, PartialOrd, + Copy, Clone, Eq, PartialEq, #scrate::__private::codec::Encode, #scrate::__private::codec::Decode, #scrate::__private::codec::MaxEncodedLen, #scrate::__private::scale_info::TypeInfo, #scrate::__private::RuntimeDebug, @@ -49,20 +62,3 @@ pub fn expand_outer_slash_reason(pallet_decls: &[Pallet], scrate: &TokenStream) #( #conversion_fns )* } } - -fn expand_conversion_fn(path: &PalletPath, variant_name: &Ident) -> TokenStream { - quote! { - impl From<#path::SlashReason> for RuntimeSlashReason { - fn from(hr: #path::SlashReason) -> Self { - RuntimeSlashReason::#variant_name(hr) - } - } - } -} - -fn expand_variant(index: u8, path: &PalletPath, variant_name: &Ident) -> TokenStream { - quote! { - #[codec(index = #index)] - #variant_name(#path::SlashReason), - } -} diff --git a/substrate/frame/support/procedural/src/pallet/parse/composite.rs b/substrate/frame/support/procedural/src/pallet/parse/composite.rs index cb554a116175c..ddcc2d07f93b3 100644 --- a/substrate/frame/support/procedural/src/pallet/parse/composite.rs +++ b/substrate/frame/support/procedural/src/pallet/parse/composite.rs @@ -15,6 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::helper; use quote::ToTokens; use syn::spanned::Spanned; @@ -108,6 +109,13 @@ impl CompositeDef { return Err(syn::Error::new(item.span(), msg)) } + let has_instance = if item.generics.params.first().is_some() { + helper::check_config_def_gen(&item.generics, item.ident.span())?; + true + } else { + false + }; + let has_derive_attr = item.attrs.iter().any(|attr| { if let syn::Meta::List(syn::MetaList { path, .. }) = &attr.meta { path.get_ident().map(|ident| ident == "derive").unwrap_or(false) @@ -119,7 +127,7 @@ impl CompositeDef { if !has_derive_attr { let derive_attr: syn::Attribute = syn::parse_quote! { #[derive( - Copy, Clone, Eq, PartialEq, Ord, PartialOrd, + Copy, Clone, Eq, PartialEq, #scrate::__private::codec::Encode, #scrate::__private::codec::Decode, #scrate::__private::codec::MaxEncodedLen, #scrate::__private::scale_info::TypeInfo, #scrate::__private::RuntimeDebug, @@ -128,6 +136,20 @@ impl CompositeDef { item.attrs.push(derive_attr); } + if has_instance { + item.attrs.push(syn::parse_quote! { + #[scale_info(skip_type_params(I))] + }); + + item.variants.push(syn::parse_quote! { + #[doc(hidden)] + #[codec(skip)] + __Ignore( + #scrate::__private::sp_std::marker::PhantomData<I>, + ) + }); + } + let composite_keyword = syn::parse2::<keyword::CompositeKeyword>(item.ident.to_token_stream())?; diff --git a/substrate/frame/support/src/lib.rs b/substrate/frame/support/src/lib.rs index 8c4b3de49b5d5..700d777a14818 100644 --- a/substrate/frame/support/src/lib.rs +++ b/substrate/frame/support/src/lib.rs @@ -1646,8 +1646,7 @@ pub mod pallet_prelude { /// the enum: /// /// ```ignore -/// Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, TypeInfo, -/// RuntimeDebug +/// Copy, Clone, Eq, PartialEq, Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug /// ``` /// /// The inverse is also true: if there are any #[derive] attributes present for the enum, then @@ -1815,6 +1814,15 @@ pub mod pallet_prelude { /// #[pallet::origin] /// pub struct Origin<T>(PhantomData<T>); /// +/// // Declare a hold reason (this is optional). +/// // +/// // Creates a hold reason for this pallet that is aggregated by `construct_runtime`. +/// // A similar enum can be defined for `FreezeReason`, `LockId` or `SlashReason`. +/// #[pallet::composite_enum] +/// pub enum HoldReason { +/// SomeHoldReason +/// } +/// /// // Declare validate_unsigned implementation (this is optional). /// #[pallet::validate_unsigned] /// impl<T: Config> ValidateUnsigned for Pallet<T> { @@ -1949,6 +1957,11 @@ pub mod pallet_prelude { /// #[pallet::origin] /// pub struct Origin<T, I = ()>(PhantomData<(T, I)>); /// +/// #[pallet::composite_enum] +/// pub enum HoldReason<I: 'static = ()> { +/// SomeHoldReason +/// } +/// /// #[pallet::validate_unsigned] /// impl<T: Config<I>, I: 'static> ValidateUnsigned for Pallet<T, I> { /// type Call = Call<T, I>; diff --git a/substrate/frame/support/test/tests/construct_runtime_ui.rs b/substrate/frame/support/test/tests/construct_runtime_ui.rs index c3197c99a72bf..0cf857e2d73c0 100644 --- a/substrate/frame/support/test/tests/construct_runtime_ui.rs +++ b/substrate/frame/support/test/tests/construct_runtime_ui.rs @@ -32,4 +32,5 @@ fn ui() { let t = trybuild::TestCases::new(); t.compile_fail("tests/construct_runtime_ui/*.rs"); + t.pass("tests/construct_runtime_ui/pass/*.rs"); } diff --git a/substrate/frame/support/test/tests/construct_runtime_ui/pass/composite_enum_instance.rs b/substrate/frame/support/test/tests/construct_runtime_ui/pass/composite_enum_instance.rs new file mode 100644 index 0000000000000..ad63708747694 --- /dev/null +++ b/substrate/frame/support/test/tests/construct_runtime_ui/pass/composite_enum_instance.rs @@ -0,0 +1,80 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// 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. + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::derive_impl; + +pub use pallet::*; + +#[frame_support::pallet(dev_mode)] +pub mod pallet { + use frame_support::pallet_prelude::*; + + // The struct on which we build all of our Pallet logic. + #[pallet::pallet] + pub struct Pallet<T, I = ()>(PhantomData<(T, I)>); + + // Your Pallet's configuration trait, representing custom external types and interfaces. + #[pallet::config] + pub trait Config<I: 'static = ()>: frame_system::Config {} + + #[pallet::composite_enum] + pub enum HoldReason<I: 'static = ()> { + SomeHoldReason + } + + #[pallet::composite_enum] + pub enum FreezeReason<I: 'static = ()> { + SomeFreezeReason + } + + #[pallet::composite_enum] + pub enum SlashReason<I: 'static = ()> { + SomeSlashReason + } + + #[pallet::composite_enum] + pub enum LockId<I: 'static = ()> { + SomeLockId + } +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Runtime { + type Block = Block; +} + +pub type Header = sp_runtime::generic::Header<u64, sp_runtime::traits::BlakeTwo256>; +pub type UncheckedExtrinsic = sp_runtime::generic::UncheckedExtrinsic<u64, RuntimeCall, (), ()>; +pub type Block = sp_runtime::generic::Block<Header, UncheckedExtrinsic>; + +frame_support::construct_runtime!( + pub struct Runtime + { + // Exclude part `Storage` in order not to check its metadata in tests. + System: frame_system, + Pallet1: pallet, + Pallet2: pallet::<Instance2>, + } +); + +impl pallet::Config for Runtime {} + +impl pallet::Config<pallet::Instance2> for Runtime {} + +fn main() {} From 832060000bbdbc87cac233bde8954ae7263f6500 Mon Sep 17 00:00:00 2001 From: Adrian Catangiu <adrian@parity.io> Date: Fri, 13 Oct 2023 10:12:30 +0300 Subject: [PATCH 39/54] sc-consensus-beefy: improve gossip logic (#1852) - Remove cached messages used for deduplication in `GossipValidator` since they're already deduplicated in upper layer `NetworkGossip`. - Add cache for "justified rounds" to quickly discard any further (even if potentially different) justifications at the gossip level, once a valid one (for a respective round) is submitted to the worker. - Add short-circuit in worker `finalize()` method to not attempt to finalize same block multiple times (for example when we get justifications for same block from multiple components like block-import, gossip or on-demand). - Change a test which had A LOT of latency in syncing blocks for some weird reason and would only run after ~150seconds. It now runs instantly. Fixes https://github.com/paritytech/polkadot-sdk/issues/1728 --- .../beefy/src/communication/gossip.rs | 156 ++++++------------ .../consensus/beefy/src/communication/mod.rs | 2 +- substrate/client/consensus/beefy/src/tests.rs | 12 +- .../client/consensus/beefy/src/worker.rs | 11 +- 4 files changed, 67 insertions(+), 114 deletions(-) diff --git a/substrate/client/consensus/beefy/src/communication/gossip.rs b/substrate/client/consensus/beefy/src/communication/gossip.rs index 8c025ca067619..342cd0511a511 100644 --- a/substrate/client/consensus/beefy/src/communication/gossip.rs +++ b/substrate/client/consensus/beefy/src/communication/gossip.rs @@ -16,11 +16,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -use std::{collections::BTreeMap, sync::Arc, time::Duration}; +use std::{collections::BTreeSet, sync::Arc, time::Duration}; use sc_network::{PeerId, ReputationChange}; use sc_network_gossip::{MessageIntent, ValidationResult, Validator, ValidatorContext}; -use sp_core::hashing::twox_64; use sp_runtime::traits::{Block, Hash, Header, NumberFor}; use codec::{Decode, DecodeAll, Encode}; @@ -115,9 +114,6 @@ where <<B::Header as Header>::Hashing as Hash>::hash(b"beefy-justifications") } -/// A type that represents hash of the message. -pub type MessageHash = [u8; 8]; - #[derive(Clone, Debug)] pub(crate) struct GossipFilterCfg<'a, B: Block> { pub start: NumberFor<B>, @@ -133,18 +129,21 @@ struct FilterInner<B: Block> { } struct Filter<B: Block> { + // specifies live rounds inner: Option<FilterInner<B>>, - live_votes: BTreeMap<NumberFor<B>, fnv::FnvHashSet<MessageHash>>, + // cache of seen valid justifications in active rounds + rounds_with_valid_proofs: BTreeSet<NumberFor<B>>, } impl<B: Block> Filter<B> { pub fn new() -> Self { - Self { inner: None, live_votes: BTreeMap::new() } + Self { inner: None, rounds_with_valid_proofs: BTreeSet::new() } } /// Update filter to new `start` and `set_id`. fn update(&mut self, cfg: GossipFilterCfg<B>) { - self.live_votes.retain(|&round, _| round >= cfg.start && round <= cfg.end); + self.rounds_with_valid_proofs + .retain(|&round| round >= cfg.start && round <= cfg.end); // only clone+overwrite big validator_set if set_id changed match self.inner.as_mut() { Some(f) if f.validator_set.id() == cfg.validator_set.id() => { @@ -203,14 +202,14 @@ impl<B: Block> Filter<B> { .unwrap_or(Consider::RejectOutOfScope) } - /// Add new _known_ `hash` to the round's known votes. - fn add_known_vote(&mut self, round: NumberFor<B>, hash: MessageHash) { - self.live_votes.entry(round).or_default().insert(hash); + /// Add new _known_ `round` to the set of seen valid justifications. + fn mark_round_as_proven(&mut self, round: NumberFor<B>) { + self.rounds_with_valid_proofs.insert(round); } - /// Check if `hash` is already part of round's known votes. - fn is_known_vote(&self, round: NumberFor<B>, hash: &MessageHash) -> bool { - self.live_votes.get(&round).map(|known| known.contains(hash)).unwrap_or(false) + /// Check if `round` is already part of seen valid justifications. + fn is_already_proven(&self, round: NumberFor<B>) -> bool { + self.rounds_with_valid_proofs.contains(&round) } fn validator_set(&self) -> Option<&ValidatorSet<AuthorityId>> { @@ -273,16 +272,13 @@ where &self, vote: VoteMessage<NumberFor<B>, AuthorityId, Signature>, sender: &PeerId, - data: &[u8], ) -> Action<B::Hash> { - let msg_hash = twox_64(data); let round = vote.commitment.block_number; let set_id = vote.commitment.validator_set_id; self.known_peers.lock().note_vote_for(*sender, round); // Verify general usefulness of the message. - // We are going to discard old votes right away (without verification) - // Also we keep track of already received votes to avoid verifying duplicates. + // We are going to discard old votes right away (without verification). { let filter = self.gossip_filter.read(); @@ -293,10 +289,6 @@ where Consider::Accept => {}, } - if filter.is_known_vote(round, &msg_hash) { - return Action::Keep(self.votes_topic, benefit::KNOWN_VOTE_MESSAGE) - } - // ensure authority is part of the set. if !filter .validator_set() @@ -309,7 +301,6 @@ where } if BeefyKeystore::verify(&vote.id, &vote.signature, &vote.commitment.encode()) { - self.gossip_filter.write().add_known_vote(round, msg_hash); Action::Keep(self.votes_topic, benefit::VOTE_MESSAGE) } else { debug!( @@ -328,34 +319,46 @@ where let (round, set_id) = proof_block_num_and_set_id::<B>(&proof); self.known_peers.lock().note_vote_for(*sender, round); - let guard = self.gossip_filter.read(); - // Verify general usefulness of the justification. - match guard.consider_finality_proof(round, set_id) { - Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE), - Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE), - Consider::RejectOutOfScope => return Action::Discard(cost::OUT_OF_SCOPE_MESSAGE), - Consider::Accept => {}, + let action = { + let guard = self.gossip_filter.read(); + + // Verify general usefulness of the justification. + match guard.consider_finality_proof(round, set_id) { + Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE), + Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE), + Consider::RejectOutOfScope => return Action::Discard(cost::OUT_OF_SCOPE_MESSAGE), + Consider::Accept => {}, + } + + if guard.is_already_proven(round) { + return Action::Discard(benefit::NOT_INTERESTED) + } + + // Verify justification signatures. + guard + .validator_set() + .map(|validator_set| { + if let Err((_, signatures_checked)) = + verify_with_validator_set::<B>(round, validator_set, &proof) + { + debug!( + target: LOG_TARGET, + "🥩 Bad signatures on message: {:?}, from: {:?}", proof, sender + ); + let mut cost = cost::INVALID_PROOF; + cost.value += + cost::PER_SIGNATURE_CHECKED.saturating_mul(signatures_checked as i32); + Action::Discard(cost) + } else { + Action::Keep(self.justifs_topic, benefit::VALIDATED_PROOF) + } + }) + .unwrap_or(Action::Discard(cost::OUT_OF_SCOPE_MESSAGE)) + }; + if matches!(action, Action::Keep(_, _)) { + self.gossip_filter.write().mark_round_as_proven(round); } - // Verify justification signatures. - guard - .validator_set() - .map(|validator_set| { - if let Err((_, signatures_checked)) = - verify_with_validator_set::<B>(round, validator_set, &proof) - { - debug!( - target: LOG_TARGET, - "🥩 Bad signatures on message: {:?}, from: {:?}", proof, sender - ); - let mut cost = cost::INVALID_PROOF; - cost.value += - cost::PER_SIGNATURE_CHECKED.saturating_mul(signatures_checked as i32); - Action::Discard(cost) - } else { - Action::Keep(self.justifs_topic, benefit::VALIDATED_PROOF) - } - }) - .unwrap_or(Action::Discard(cost::OUT_OF_SCOPE_MESSAGE)) + action } } @@ -375,7 +378,7 @@ where ) -> ValidationResult<B::Hash> { let raw = data; let action = match GossipMessage::<B>::decode_all(&mut data) { - Ok(GossipMessage::Vote(msg)) => self.validate_vote(msg, sender, raw), + Ok(GossipMessage::Vote(msg)) => self.validate_vote(msg, sender), Ok(GossipMessage::FinalityProof(proof)) => self.validate_finality_proof(proof, sender), Err(e) => { debug!(target: LOG_TARGET, "Error decoding message: {}", e); @@ -483,41 +486,6 @@ pub(crate) mod tests { }; use sp_keystore::{testing::MemoryKeystore, Keystore}; - #[test] - fn known_votes_insert_remove() { - let mut filter = Filter::<Block>::new(); - let msg_hash = twox_64(b"data"); - let keys = vec![Keyring::Alice.public()]; - let validator_set = ValidatorSet::<AuthorityId>::new(keys.clone(), 1).unwrap(); - - filter.add_known_vote(1, msg_hash); - filter.add_known_vote(1, msg_hash); - filter.add_known_vote(2, msg_hash); - assert_eq!(filter.live_votes.len(), 2); - - filter.add_known_vote(3, msg_hash); - assert!(filter.is_known_vote(3, &msg_hash)); - assert!(!filter.is_known_vote(3, &twox_64(b"other"))); - assert!(!filter.is_known_vote(4, &msg_hash)); - assert_eq!(filter.live_votes.len(), 3); - - assert!(filter.inner.is_none()); - assert_eq!(filter.consider_vote(1, 1), Consider::RejectOutOfScope); - - filter.update(GossipFilterCfg { start: 3, end: 10, validator_set: &validator_set }); - assert_eq!(filter.live_votes.len(), 1); - assert!(filter.live_votes.contains_key(&3)); - assert_eq!(filter.consider_vote(2, 1), Consider::RejectPast); - assert_eq!(filter.consider_vote(3, 1), Consider::Accept); - assert_eq!(filter.consider_vote(4, 1), Consider::Accept); - assert_eq!(filter.consider_vote(20, 1), Consider::RejectFuture); - assert_eq!(filter.consider_vote(4, 2), Consider::RejectFuture); - - let validator_set = ValidatorSet::<AuthorityId>::new(keys, 2).unwrap(); - filter.update(GossipFilterCfg { start: 5, end: 10, validator_set: &validator_set }); - assert!(filter.live_votes.is_empty()); - } - struct TestContext; impl<B: sp_runtime::traits::Block> ValidatorContext<B> for TestContext { fn broadcast_topic(&mut self, _topic: B::Hash, _force: bool) { @@ -610,20 +578,6 @@ pub(crate) mod tests { assert!(matches!(res, ValidationResult::ProcessAndKeep(_))); expected_report.cost_benefit = benefit::VOTE_MESSAGE; assert_eq!(report_stream.try_recv().unwrap(), expected_report); - assert_eq!( - gv.gossip_filter - .read() - .live_votes - .get(&vote.commitment.block_number) - .map(|x| x.len()), - Some(1) - ); - - // second time we should hit the cache - let res = gv.validate(&mut context, &sender, &encoded); - assert!(matches!(res, ValidationResult::ProcessAndKeep(_))); - expected_report.cost_benefit = benefit::KNOWN_VOTE_MESSAGE; - assert_eq!(report_stream.try_recv().unwrap(), expected_report); // reject vote, voter not in validator set let mut bad_vote = vote.clone(); @@ -692,7 +646,7 @@ pub(crate) mod tests { // reject proof, bad signatures (Bob instead of Alice) let bad_validator_set = ValidatorSet::<AuthorityId>::new(vec![Keyring::Bob.public()], 0).unwrap(); - let proof = dummy_proof(20, &bad_validator_set); + let proof = dummy_proof(21, &bad_validator_set); let encoded_proof = GossipMessage::<Block>::FinalityProof(proof).encode(); let res = gv.validate(&mut context, &sender, &encoded_proof); assert!(matches!(res, ValidationResult::Discard)); diff --git a/substrate/client/consensus/beefy/src/communication/mod.rs b/substrate/client/consensus/beefy/src/communication/mod.rs index 7f9535bfc23f1..10a6071aae658 100644 --- a/substrate/client/consensus/beefy/src/communication/mod.rs +++ b/substrate/client/consensus/beefy/src/communication/mod.rs @@ -102,7 +102,7 @@ mod cost { mod benefit { use sc_network::ReputationChange as Rep; pub(super) const VOTE_MESSAGE: Rep = Rep::new(100, "BEEFY: Round vote message"); - pub(super) const KNOWN_VOTE_MESSAGE: Rep = Rep::new(50, "BEEFY: Known vote"); + pub(super) const NOT_INTERESTED: Rep = Rep::new(10, "BEEFY: Not interested in round"); pub(super) const VALIDATED_PROOF: Rep = Rep::new(100, "BEEFY: Justification"); } diff --git a/substrate/client/consensus/beefy/src/tests.rs b/substrate/client/consensus/beefy/src/tests.rs index 3bb65e9d57f43..90b63c9cd4462 100644 --- a/substrate/client/consensus/beefy/src/tests.rs +++ b/substrate/client/consensus/beefy/src/tests.rs @@ -1302,7 +1302,7 @@ async fn gossipped_finality_proofs() { // Only Alice and Bob are running the voter -> finality threshold not reached let peers = [BeefyKeyring::Alice, BeefyKeyring::Bob]; let validator_set = ValidatorSet::new(make_beefy_ids(&validators), 0).unwrap(); - let session_len = 30; + let session_len = 10; let min_block_delta = 1; let mut net = BeefyTestNet::new(3); @@ -1332,14 +1332,8 @@ async fn gossipped_finality_proofs() { let net = Arc::new(Mutex::new(net)); - // Pump net + Charlie gossip to see peers. - let timeout = Box::pin(tokio::time::sleep(Duration::from_millis(200))); - let gossip_engine_pump = &mut charlie_gossip_engine; - let pump_with_timeout = future::select(gossip_engine_pump, timeout); - run_until(pump_with_timeout, &net).await; - - // push 10 blocks - let hashes = net.lock().generate_blocks_and_sync(10, session_len, &validator_set, true).await; + // push 42 blocks + let hashes = net.lock().generate_blocks_and_sync(42, session_len, &validator_set, true).await; let peers = peers.into_iter().enumerate(); diff --git a/substrate/client/consensus/beefy/src/worker.rs b/substrate/client/consensus/beefy/src/worker.rs index a239e34030c27..309d8c5135bef 100644 --- a/substrate/client/consensus/beefy/src/worker.rs +++ b/substrate/client/consensus/beefy/src/worker.rs @@ -604,6 +604,11 @@ where VersionedFinalityProof::V1(ref sc) => sc.commitment.block_number, }; + if block_num <= self.persisted_state.voting_oracle.best_beefy_block { + // we've already finalized this round before, short-circuit. + return Ok(()) + } + // Finalize inner round and update voting_oracle state. self.persisted_state.voting_oracle.finalize(block_num)?; @@ -629,7 +634,7 @@ where self.backend .append_justification(hash, (BEEFY_ENGINE_ID, finality_proof.encode())) }) { - error!( + debug!( target: LOG_TARGET, "🥩 Error {:?} on appending justification: {:?}", e, finality_proof ); @@ -648,7 +653,7 @@ where } /// Handle previously buffered justifications, that now land in the voting interval. - fn try_pending_justififactions(&mut self) -> Result<(), Error> { + fn try_pending_justifications(&mut self) -> Result<(), Error> { // Interval of blocks for which we can process justifications and votes right now. let (start, end) = self.voting_oracle().accepted_interval()?; // Process pending justifications. @@ -782,7 +787,7 @@ where fn process_new_state(&mut self) { // Handle pending justifications and/or votes for now GRANDPA finalized blocks. - if let Err(err) = self.try_pending_justififactions() { + if let Err(err) = self.try_pending_justifications() { debug!(target: LOG_TARGET, "🥩 {}", err); } From 82bfe28424464ccafbe1ddff2d79491e0ddf7080 Mon Sep 17 00:00:00 2001 From: Adrian Catangiu <adrian@parity.io> Date: Fri, 13 Oct 2023 11:37:48 +0300 Subject: [PATCH 40/54] frame: use derive-impl for beefy and mmr pallets (#1867) Part of #171 --- substrate/frame/beefy-mmr/src/mock.rs | 30 +++------------- substrate/frame/beefy/src/mock.rs | 36 ++++--------------- .../frame/merkle-mountain-range/src/mock.rs | 31 ++-------------- 3 files changed, 13 insertions(+), 84 deletions(-) diff --git a/substrate/frame/beefy-mmr/src/mock.rs b/substrate/frame/beefy-mmr/src/mock.rs index b2d8758a04be6..b85096813deca 100644 --- a/substrate/frame/beefy-mmr/src/mock.rs +++ b/substrate/frame/beefy-mmr/src/mock.rs @@ -19,16 +19,15 @@ use std::vec; use codec::Encode; use frame_support::{ - construct_runtime, parameter_types, - traits::{ConstU16, ConstU32, ConstU64}, + construct_runtime, derive_impl, parameter_types, + traits::{ConstU32, ConstU64}, }; use sp_consensus_beefy::mmr::MmrLeafVersion; -use sp_core::H256; use sp_io::TestExternalities; use sp_runtime::{ app_crypto::ecdsa::Public, impl_opaque_keys, - traits::{BlakeTwo256, ConvertInto, IdentityLookup, Keccak256, OpaqueKeys}, + traits::{ConvertInto, Keccak256, OpaqueKeys}, BuildStorage, }; use sp_state_machine::BasicExternalities; @@ -58,30 +57,9 @@ construct_runtime!( } ); +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type Nonce = u64; - type Hash = H256; - type RuntimeCall = RuntimeCall; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup<Self::AccountId>; type Block = Block; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = ConstU64<250>; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = (); - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = ConstU16<42>; - type OnSetCode = (); - type MaxConsumers = ConstU32<16>; } impl pallet_session::Config for Test { diff --git a/substrate/frame/beefy/src/mock.rs b/substrate/frame/beefy/src/mock.rs index b55a65dbd73af..9480fd0406d78 100644 --- a/substrate/frame/beefy/src/mock.rs +++ b/substrate/frame/beefy/src/mock.rs @@ -22,19 +22,15 @@ use frame_election_provider_support::{ onchain, SequentialPhragmen, }; use frame_support::{ - construct_runtime, parameter_types, - traits::{ConstU16, ConstU32, ConstU64, KeyOwnerProofSystem, OnFinalize, OnInitialize}, + construct_runtime, derive_impl, parameter_types, + traits::{ConstU32, ConstU64, KeyOwnerProofSystem, OnFinalize, OnInitialize}, }; use pallet_session::historical as pallet_session_historical; -use sp_core::{crypto::KeyTypeId, ConstU128, H256}; +use sp_core::{crypto::KeyTypeId, ConstU128}; use sp_io::TestExternalities; use sp_runtime::{ - app_crypto::ecdsa::Public, - curve::PiecewiseLinear, - impl_opaque_keys, - testing::TestXt, - traits::{BlakeTwo256, IdentityLookup, OpaqueKeys}, - BuildStorage, Perbill, + app_crypto::ecdsa::Public, curve::PiecewiseLinear, impl_opaque_keys, testing::TestXt, + traits::OpaqueKeys, BuildStorage, Perbill, }; use sp_staking::{EraIndex, SessionIndex}; use sp_state_machine::BasicExternalities; @@ -69,30 +65,10 @@ construct_runtime!( } ); +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type Nonce = u64; - type Hash = H256; - type RuntimeCall = RuntimeCall; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup<Self::AccountId>; type Block = Block; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = ConstU64<250>; - type Version = (); - type PalletInfo = PalletInfo; type AccountData = pallet_balances::AccountData<u128>; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = ConstU16<42>; - type OnSetCode = (); - type MaxConsumers = ConstU32<16>; } impl<C> frame_system::offchain::SendTransactionTypes<C> for Test diff --git a/substrate/frame/merkle-mountain-range/src/mock.rs b/substrate/frame/merkle-mountain-range/src/mock.rs index ecc254278bf0f..d3cb4e5702972 100644 --- a/substrate/frame/merkle-mountain-range/src/mock.rs +++ b/substrate/frame/merkle-mountain-range/src/mock.rs @@ -19,13 +19,9 @@ use crate as pallet_mmr; use crate::*; use codec::{Decode, Encode}; -use frame_support::{ - parameter_types, - traits::{ConstU32, ConstU64}, -}; -use sp_core::H256; +use frame_support::{derive_impl, parameter_types}; use sp_mmr_primitives::{Compact, LeafDataProvider}; -use sp_runtime::traits::{BlakeTwo256, IdentityLookup, Keccak256}; +use sp_runtime::traits::Keccak256; type Block = frame_system::mocking::MockBlock<Test>; @@ -37,30 +33,9 @@ frame_support::construct_runtime!( } ); +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Nonce = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = sp_core::sr25519::Public; - type Lookup = IdentityLookup<Self::AccountId>; type Block = Block; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = ConstU64<250>; - type DbWeight = (); - type BlockWeights = (); - type BlockLength = (); - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = (); - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = ConstU32<16>; } impl Config for Test { From 681e7bbfb2e24b41f6c45825532e6a976596d9db Mon Sep 17 00:00:00 2001 From: Julian Eager <eagr@tutanota.com> Date: Fri, 13 Oct 2023 21:52:04 +0800 Subject: [PATCH 41/54] Check executor params coherence (#1774) Co-authored-by: Marcin S <marcin@realemail.net> --- .../node/core/candidate-validation/src/lib.rs | 11 +- .../node/core/pvf/common/src/executor_intf.rs | 17 +- .../node/core/pvf/execute-worker/src/lib.rs | 6 +- polkadot/primitives/src/lib.rs | 26 +-- polkadot/primitives/src/v6/executor_params.rs | 198 +++++++++++++++++- polkadot/primitives/src/v6/mod.rs | 2 +- .../runtime/parachains/src/configuration.rs | 11 +- 7 files changed, 231 insertions(+), 40 deletions(-) diff --git a/polkadot/node/core/candidate-validation/src/lib.rs b/polkadot/node/core/candidate-validation/src/lib.rs index 3281fcc59a158..21a7121d47bd0 100644 --- a/polkadot/node/core/candidate-validation/src/lib.rs +++ b/polkadot/node/core/candidate-validation/src/lib.rs @@ -44,6 +44,10 @@ use polkadot_parachain_primitives::primitives::{ ValidationParams, ValidationResult as WasmValidationResult, }; use polkadot_primitives::{ + executor_params::{ + DEFAULT_APPROVAL_EXECUTION_TIMEOUT, DEFAULT_BACKING_EXECUTION_TIMEOUT, + DEFAULT_LENIENT_PREPARATION_TIMEOUT, DEFAULT_PRECHECK_PREPARATION_TIMEOUT, + }, CandidateCommitments, CandidateDescriptor, CandidateReceipt, ExecutorParams, Hash, OccupiedCoreAssumption, PersistedValidationData, PvfExecTimeoutKind, PvfPrepTimeoutKind, ValidationCode, ValidationCodeHash, @@ -83,13 +87,6 @@ const PVF_APPROVAL_EXECUTION_RETRY_DELAY: Duration = Duration::from_secs(3); #[cfg(test)] const PVF_APPROVAL_EXECUTION_RETRY_DELAY: Duration = Duration::from_millis(200); -// Default PVF timeouts. Must never be changed! Use executor environment parameters in -// `session_info` pallet to adjust them. See also `PvfTimeoutKind` docs. -const DEFAULT_PRECHECK_PREPARATION_TIMEOUT: Duration = Duration::from_secs(60); -const DEFAULT_LENIENT_PREPARATION_TIMEOUT: Duration = Duration::from_secs(360); -const DEFAULT_BACKING_EXECUTION_TIMEOUT: Duration = Duration::from_secs(2); -const DEFAULT_APPROVAL_EXECUTION_TIMEOUT: Duration = Duration::from_secs(12); - /// Configuration for the candidate validation subsystem #[derive(Clone)] pub struct Config { diff --git a/polkadot/node/core/pvf/common/src/executor_intf.rs b/polkadot/node/core/pvf/common/src/executor_intf.rs index 79839149ebdf1..508a12998fc9d 100644 --- a/polkadot/node/core/pvf/common/src/executor_intf.rs +++ b/polkadot/node/core/pvf/common/src/executor_intf.rs @@ -16,7 +16,10 @@ //! Interface to the Substrate Executor -use polkadot_primitives::{ExecutorParam, ExecutorParams}; +use polkadot_primitives::{ + executor_params::{DEFAULT_LOGICAL_STACK_MAX, DEFAULT_NATIVE_STACK_MAX}, + ExecutorParam, ExecutorParams, +}; use sc_executor_common::{ error::WasmError, runtime_blob::RuntimeBlob, @@ -42,9 +45,6 @@ use std::any::{Any, TypeId}; const DEFAULT_HEAP_PAGES_ESTIMATE: u32 = 32; const EXTRA_HEAP_PAGES: u32 = 2048; -/// The number of bytes devoted for the stack during wasm execution of a PVF. -pub const NATIVE_STACK_MAX: u32 = 256 * 1024 * 1024; - // VALUES OF THE DEFAULT CONFIGURATION SHOULD NEVER BE CHANGED // They are used as base values for the execution environment parametrization. // To overwrite them, add new ones to `EXECUTOR_PARAMS` in the `session_info` pallet and perform @@ -73,8 +73,8 @@ pub const DEFAULT_CONFIG: Config = Config { // also increase the native 256x. This hopefully should preclude wasm code from reaching // the stack limit set by the wasmtime. deterministic_stack_limit: Some(DeterministicStackLimit { - logical_max: 65536, - native_stack_max: NATIVE_STACK_MAX, + logical_max: DEFAULT_LOGICAL_STACK_MAX, + native_stack_max: DEFAULT_NATIVE_STACK_MAX, }), canonicalize_nans: true, // Rationale for turning the multi-threaded compilation off is to make the preparation time @@ -106,8 +106,9 @@ pub fn params_to_wasmtime_semantics(par: &ExecutorParams) -> Result<Semantics, S for p in par.iter() { match p { ExecutorParam::MaxMemoryPages(max_pages) => - sem.heap_alloc_strategy = - HeapAllocStrategy::Dynamic { maximum_pages: Some(*max_pages) }, + sem.heap_alloc_strategy = HeapAllocStrategy::Dynamic { + maximum_pages: Some((*max_pages).saturating_add(DEFAULT_HEAP_PAGES_ESTIMATE)), + }, ExecutorParam::StackLogicalMax(slm) => stack_limit.logical_max = *slm, ExecutorParam::StackNativeMax(snm) => stack_limit.native_stack_max = *snm, ExecutorParam::WasmExtBulkMemory => sem.wasm_bulk_memory = true, diff --git a/polkadot/node/core/pvf/execute-worker/src/lib.rs b/polkadot/node/core/pvf/execute-worker/src/lib.rs index 02eaedb96f28e..65185b6f45f36 100644 --- a/polkadot/node/core/pvf/execute-worker/src/lib.rs +++ b/polkadot/node/core/pvf/execute-worker/src/lib.rs @@ -27,7 +27,6 @@ use parity_scale_codec::{Decode, Encode}; use polkadot_node_core_pvf_common::{ error::InternalValidationError, execute::{Handshake, Response}, - executor_intf::NATIVE_STACK_MAX, framed_recv_blocking, framed_send_blocking, worker::{ cpu_time_monitor_loop, stringify_panic_payload, @@ -36,6 +35,7 @@ use polkadot_node_core_pvf_common::{ }, }; use polkadot_parachain_primitives::primitives::ValidationResult; +use polkadot_primitives::executor_params::DEFAULT_NATIVE_STACK_MAX; use std::{ os::unix::net::UnixStream, path::PathBuf, @@ -69,7 +69,7 @@ use tokio::io; // // Typically on Linux the main thread gets the stack size specified by the `ulimit` and // typically it's configured to 8 MiB. Rust's spawned threads are 2 MiB. OTOH, the -// NATIVE_STACK_MAX is set to 256 MiB. Not nearly enough. +// DEFAULT_NATIVE_STACK_MAX is set to 256 MiB. Not nearly enough. // // Hence we need to increase it. The simplest way to fix that is to spawn a thread with the desired // stack limit. @@ -78,7 +78,7 @@ use tokio::io; // // The default Rust thread stack limit 2 MiB + 256 MiB wasm stack. /// The stack size for the execute thread. -pub const EXECUTE_THREAD_STACK_SIZE: usize = 2 * 1024 * 1024 + NATIVE_STACK_MAX as usize; +pub const EXECUTE_THREAD_STACK_SIZE: usize = 2 * 1024 * 1024 + DEFAULT_NATIVE_STACK_MAX as usize; fn recv_handshake(stream: &mut UnixStream) -> io::Result<Handshake> { let handshake_enc = framed_recv_blocking(stream)?; diff --git a/polkadot/primitives/src/lib.rs b/polkadot/primitives/src/lib.rs index 5adb6d2531347..4ba8b8b031fcc 100644 --- a/polkadot/primitives/src/lib.rs +++ b/polkadot/primitives/src/lib.rs @@ -35,19 +35,19 @@ pub mod runtime_api; // Primitives requiring versioning must not be exported and must be referred by an exact version. pub use v6::{ async_backing, byzantine_threshold, check_candidate_backing, collator_signature_payload, - effective_minimum_backing_votes, metric_definitions, slashing, supermajority_threshold, - well_known_keys, AbridgedHostConfiguration, AbridgedHrmpChannel, AccountId, AccountIndex, - AccountPublic, ApprovalVote, AssignmentId, AsyncBackingParams, AuthorityDiscoveryId, - AvailabilityBitfield, BackedCandidate, Balance, BlakeTwo256, Block, BlockId, BlockNumber, - CandidateCommitments, CandidateDescriptor, CandidateEvent, CandidateHash, CandidateIndex, - CandidateReceipt, CheckedDisputeStatementSet, CheckedMultiDisputeStatementSet, CollatorId, - CollatorSignature, CommittedCandidateReceipt, CompactStatement, ConsensusLog, CoreIndex, - CoreState, DisputeState, DisputeStatement, DisputeStatementSet, DownwardMessage, EncodeAs, - ExecutorParam, ExecutorParams, ExecutorParamsHash, ExplicitDisputeStatement, GroupIndex, - GroupRotationInfo, Hash, HashT, HeadData, Header, HorizontalMessages, HrmpChannelId, Id, - InboundDownwardMessage, InboundHrmpMessage, IndexedVec, InherentData, - InvalidDisputeStatementKind, Moment, MultiDisputeStatementSet, Nonce, OccupiedCore, - OccupiedCoreAssumption, OutboundHrmpMessage, ParathreadClaim, ParathreadEntry, + effective_minimum_backing_votes, executor_params, metric_definitions, slashing, + supermajority_threshold, well_known_keys, AbridgedHostConfiguration, AbridgedHrmpChannel, + AccountId, AccountIndex, AccountPublic, ApprovalVote, AssignmentId, AsyncBackingParams, + AuthorityDiscoveryId, AvailabilityBitfield, BackedCandidate, Balance, BlakeTwo256, Block, + BlockId, BlockNumber, CandidateCommitments, CandidateDescriptor, CandidateEvent, CandidateHash, + CandidateIndex, CandidateReceipt, CheckedDisputeStatementSet, CheckedMultiDisputeStatementSet, + CollatorId, CollatorSignature, CommittedCandidateReceipt, CompactStatement, ConsensusLog, + CoreIndex, CoreState, DisputeState, DisputeStatement, DisputeStatementSet, DownwardMessage, + EncodeAs, ExecutorParam, ExecutorParamError, ExecutorParams, ExecutorParamsHash, + ExplicitDisputeStatement, GroupIndex, GroupRotationInfo, Hash, HashT, HeadData, Header, + HorizontalMessages, HrmpChannelId, Id, InboundDownwardMessage, InboundHrmpMessage, IndexedVec, + InherentData, InvalidDisputeStatementKind, Moment, MultiDisputeStatementSet, Nonce, + OccupiedCore, OccupiedCoreAssumption, OutboundHrmpMessage, ParathreadClaim, ParathreadEntry, PersistedValidationData, PvfCheckStatement, PvfExecTimeoutKind, PvfPrepTimeoutKind, RuntimeMetricLabel, RuntimeMetricLabelValue, RuntimeMetricLabelValues, RuntimeMetricLabels, RuntimeMetricOp, RuntimeMetricUpdate, ScheduledCore, ScrapedOnChainVotes, SessionIndex, diff --git a/polkadot/primitives/src/v6/executor_params.rs b/polkadot/primitives/src/v6/executor_params.rs index 6fbf3037fd6cd..8ea0889bd23b0 100644 --- a/polkadot/primitives/src/v6/executor_params.rs +++ b/polkadot/primitives/src/v6/executor_params.rs @@ -26,28 +26,83 @@ use parity_scale_codec::{Decode, Encode}; use polkadot_core_primitives::Hash; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; -use sp_std::{ops::Deref, time::Duration, vec, vec::Vec}; +use sp_std::{collections::btree_map::BTreeMap, ops::Deref, time::Duration, vec, vec::Vec}; + +/// Default maximum number of wasm values allowed for the stack during execution of a PVF. +pub const DEFAULT_LOGICAL_STACK_MAX: u32 = 65536; +/// Default maximum number of bytes devoted for the stack during execution of a PVF. +pub const DEFAULT_NATIVE_STACK_MAX: u32 = 256 * 1024 * 1024; + +/// The limit of [`ExecutorParam::MaxMemoryPages`]. +pub const MEMORY_PAGES_MAX: u32 = 65536; +/// The lower bound of [`ExecutorParam::StackLogicalMax`]. +pub const LOGICAL_MAX_LO: u32 = 1024; +/// The upper bound of [`ExecutorParam::StackLogicalMax`]. +pub const LOGICAL_MAX_HI: u32 = 2 * 65536; +/// The lower bound of [`ExecutorParam::PrecheckingMaxMemory`]. +pub const PRECHECK_MEM_MAX_LO: u64 = 256 * 1024 * 1024; +/// The upper bound of [`ExecutorParam::PrecheckingMaxMemory`]. +pub const PRECHECK_MEM_MAX_HI: u64 = 16 * 1024 * 1024 * 1024; + +// Default PVF timeouts. Must never be changed! Use executor environment parameters to adjust them. +// See also `PvfPrepTimeoutKind` and `PvfExecTimeoutKind` docs. + +/// Default PVF preparation timeout for prechecking requests. +pub const DEFAULT_PRECHECK_PREPARATION_TIMEOUT: Duration = Duration::from_secs(60); +/// Default PVF preparation timeout for execution requests. +pub const DEFAULT_LENIENT_PREPARATION_TIMEOUT: Duration = Duration::from_secs(360); +/// Default PVF execution timeout for backing. +pub const DEFAULT_BACKING_EXECUTION_TIMEOUT: Duration = Duration::from_secs(2); +/// Default PVF execution timeout for approval or disputes. +pub const DEFAULT_APPROVAL_EXECUTION_TIMEOUT: Duration = Duration::from_secs(12); + +const DEFAULT_PRECHECK_PREPARATION_TIMEOUT_MS: u64 = + DEFAULT_PRECHECK_PREPARATION_TIMEOUT.as_millis() as u64; +const DEFAULT_LENIENT_PREPARATION_TIMEOUT_MS: u64 = + DEFAULT_LENIENT_PREPARATION_TIMEOUT.as_millis() as u64; +const DEFAULT_BACKING_EXECUTION_TIMEOUT_MS: u64 = + DEFAULT_BACKING_EXECUTION_TIMEOUT.as_millis() as u64; +const DEFAULT_APPROVAL_EXECUTION_TIMEOUT_MS: u64 = + DEFAULT_APPROVAL_EXECUTION_TIMEOUT.as_millis() as u64; /// The different executor parameters for changing the execution environment semantics. #[derive(Clone, Debug, Encode, Decode, PartialEq, Eq, TypeInfo, Serialize, Deserialize)] pub enum ExecutorParam { /// Maximum number of memory pages (64KiB bytes per page) the executor can allocate. + /// A valid value lies within (0, 65536]. #[codec(index = 1)] MaxMemoryPages(u32), - /// Wasm logical stack size limit (max. number of Wasm values on stack) + /// Wasm logical stack size limit (max. number of Wasm values on stack). + /// A valid value lies within [[`LOGICAL_MAX_LO`], [`LOGICAL_MAX_HI`]]. + /// + /// For WebAssembly, the stack limit is subject to implementations, meaning that it may vary on + /// different platforms. However, we want execution to be deterministic across machines of + /// different architectures, including failures like stack overflow. For deterministic + /// overflow, we rely on a **logical** limit, the maximum number of values allowed to be pushed + /// on the stack. #[codec(index = 2)] StackLogicalMax(u32), - /// Executor machine stack size limit, in bytes + /// Executor machine stack size limit, in bytes. + /// If `StackLogicalMax` is also present, a valid value should not fall below + /// 128 * `StackLogicalMax`. + /// + /// For deterministic overflow, `StackLogicalMax` should be reached before the native stack is + /// exhausted. #[codec(index = 3)] StackNativeMax(u32), /// Max. amount of memory the preparation worker is allowed to use during - /// pre-checking, in bytes + /// pre-checking, in bytes. + /// Valid max. memory ranges from [`PRECHECK_MEM_MAX_LO`] to [`PRECHECK_MEM_MAX_HI`]. #[codec(index = 4)] PrecheckingMaxMemory(u64), - /// PVF preparation timeouts, millisec + /// PVF preparation timeouts, in millisecond. + /// Always ensure that `precheck_timeout` < `lenient_timeout`. + /// When absent, the default values will be used. #[codec(index = 5)] PvfPrepTimeout(PvfPrepTimeoutKind, u64), - /// PVF execution timeouts, millisec + /// PVF execution timeouts, in millisecond. + /// Always ensure that `backing_timeout` < `approval_timeout`. + /// When absent, the default values will be used. #[codec(index = 6)] PvfExecTimeout(PvfExecTimeoutKind, u64), /// Enables WASM bulk memory proposal @@ -55,6 +110,17 @@ pub enum ExecutorParam { WasmExtBulkMemory, } +/// Possible inconsistencies of executor params. +#[derive(Debug)] +pub enum ExecutorParamError { + /// A param is duplicated. + DuplicatedParam(&'static str), + /// A param value exceeds its limitation. + OutsideLimit(&'static str), + /// Two param values are incompatible or senseless when put together. + IncompatibleValues(&'static str, &'static str), +} + /// Unit type wrapper around [`type@Hash`] that represents an execution parameter set hash. /// /// This type is produced by [`ExecutorParams::hash`]. @@ -130,6 +196,126 @@ impl ExecutorParams { } None } + + /// Check params coherence. + pub fn check_consistency(&self) -> Result<(), ExecutorParamError> { + use ExecutorParam::*; + use ExecutorParamError::*; + + let mut seen = BTreeMap::<&str, u64>::new(); + + macro_rules! check { + ($param:ident, $val:expr $(,)?) => { + if seen.contains_key($param) { + return Err(DuplicatedParam($param)) + } + seen.insert($param, $val as u64); + }; + + // should check existence before range + ($param:ident, $val:expr, $out_of_limit:expr $(,)?) => { + if seen.contains_key($param) { + return Err(DuplicatedParam($param)) + } + if $out_of_limit { + return Err(OutsideLimit($param)) + } + seen.insert($param, $val as u64); + }; + } + + for param in &self.0 { + // should ensure to be unique + let param_ident = match *param { + MaxMemoryPages(_) => "MaxMemoryPages", + StackLogicalMax(_) => "StackLogicalMax", + StackNativeMax(_) => "StackNativeMax", + PrecheckingMaxMemory(_) => "PrecheckingMaxMemory", + PvfPrepTimeout(kind, _) => match kind { + PvfPrepTimeoutKind::Precheck => "PvfPrepTimeoutKind::Precheck", + PvfPrepTimeoutKind::Lenient => "PvfPrepTimeoutKind::Lenient", + }, + PvfExecTimeout(kind, _) => match kind { + PvfExecTimeoutKind::Backing => "PvfExecTimeoutKind::Backing", + PvfExecTimeoutKind::Approval => "PvfExecTimeoutKind::Approval", + }, + WasmExtBulkMemory => "WasmExtBulkMemory", + }; + + match *param { + MaxMemoryPages(val) => { + check!(param_ident, val, val == 0 || val > MEMORY_PAGES_MAX,); + }, + + StackLogicalMax(val) => { + check!(param_ident, val, val < LOGICAL_MAX_LO || val > LOGICAL_MAX_HI,); + }, + + StackNativeMax(val) => { + check!(param_ident, val); + }, + + PrecheckingMaxMemory(val) => { + check!( + param_ident, + val, + val < PRECHECK_MEM_MAX_LO || val > PRECHECK_MEM_MAX_HI, + ); + }, + + PvfPrepTimeout(_, val) => { + check!(param_ident, val); + }, + + PvfExecTimeout(_, val) => { + check!(param_ident, val); + }, + + WasmExtBulkMemory => { + check!(param_ident, 1); + }, + } + } + + if let (Some(lm), Some(nm)) = ( + seen.get("StackLogicalMax").or(Some(&(DEFAULT_LOGICAL_STACK_MAX as u64))), + seen.get("StackNativeMax").or(Some(&(DEFAULT_NATIVE_STACK_MAX as u64))), + ) { + if *nm < 128 * *lm { + return Err(IncompatibleValues("StackLogicalMax", "StackNativeMax")) + } + } + + if let (Some(precheck), Some(lenient)) = ( + seen.get("PvfPrepTimeoutKind::Precheck") + .or(Some(&DEFAULT_PRECHECK_PREPARATION_TIMEOUT_MS)), + seen.get("PvfPrepTimeoutKind::Lenient") + .or(Some(&DEFAULT_LENIENT_PREPARATION_TIMEOUT_MS)), + ) { + if *precheck >= *lenient { + return Err(IncompatibleValues( + "PvfPrepTimeoutKind::Precheck", + "PvfPrepTimeoutKind::Lenient", + )) + } + } + + if let (Some(backing), Some(approval)) = ( + seen.get("PvfExecTimeoutKind::Backing") + .or(Some(&DEFAULT_BACKING_EXECUTION_TIMEOUT_MS)), + seen.get("PvfExecTimeoutKind::Approval") + .or(Some(&DEFAULT_APPROVAL_EXECUTION_TIMEOUT_MS)), + ) { + if *backing >= *approval { + return Err(IncompatibleValues( + "PvfExecTimeoutKind::Backing", + "PvfExecTimeoutKind::Approval", + )) + } + } + + Ok(()) + } } impl Deref for ExecutorParams { diff --git a/polkadot/primitives/src/v6/mod.rs b/polkadot/primitives/src/v6/mod.rs index cf90083551788..9371b3db406b3 100644 --- a/polkadot/primitives/src/v6/mod.rs +++ b/polkadot/primitives/src/v6/mod.rs @@ -62,7 +62,7 @@ pub mod executor_params; pub mod slashing; pub use async_backing::AsyncBackingParams; -pub use executor_params::{ExecutorParam, ExecutorParams, ExecutorParamsHash}; +pub use executor_params::{ExecutorParam, ExecutorParamError, ExecutorParams, ExecutorParamsHash}; mod metrics; pub use metrics::{ diff --git a/polkadot/runtime/parachains/src/configuration.rs b/polkadot/runtime/parachains/src/configuration.rs index f53f986a553fe..a65fda370488a 100644 --- a/polkadot/runtime/parachains/src/configuration.rs +++ b/polkadot/runtime/parachains/src/configuration.rs @@ -26,8 +26,9 @@ use polkadot_parachain_primitives::primitives::{ MAX_HORIZONTAL_MESSAGE_NUM, MAX_UPWARD_MESSAGE_NUM, }; use primitives::{ - AsyncBackingParams, Balance, ExecutorParams, SessionIndex, LEGACY_MIN_BACKING_VOTES, - MAX_CODE_SIZE, MAX_HEAD_DATA_SIZE, MAX_POV_SIZE, ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE, + AsyncBackingParams, Balance, ExecutorParamError, ExecutorParams, SessionIndex, + LEGACY_MIN_BACKING_VOTES, MAX_CODE_SIZE, MAX_HEAD_DATA_SIZE, MAX_POV_SIZE, + ON_DEMAND_DEFAULT_QUEUE_MAX_SIZE, }; use sp_runtime::{traits::Zero, Perbill}; use sp_std::prelude::*; @@ -348,6 +349,8 @@ pub enum InconsistentError<BlockNumber> { MaxHrmpInboundChannelsExceeded, /// `minimum_backing_votes` is set to zero. ZeroMinimumBackingVotes, + /// `executor_params` are inconsistent. + InconsistentExecutorParams { inner: ExecutorParamError }, } impl<BlockNumber> HostConfiguration<BlockNumber> @@ -432,6 +435,10 @@ where return Err(ZeroMinimumBackingVotes) } + if let Err(inner) = self.executor_params.check_consistency() { + return Err(InconsistentExecutorParams { inner }) + } + Ok(()) } From 24840290af20ef67956c48b7548ab184d45b8889 Mon Sep 17 00:00:00 2001 From: 0xmovses <35300528+0xmovses@users.noreply.github.com> Date: Fri, 13 Oct 2023 21:54:45 +0100 Subject: [PATCH 42/54] Refactor alliance benchmarks to v2 (#1868) - This PR refactors `alliance/src/benchmarkings.rs` to use benchmarking v2. These changes are needed to improve the readability and maintainability of the benchmarking code. - No known issue to backlink. ## Local Testing 1. `cargo build --features runtime-benchmarks` 2. `cargo run --locked --release -p node-cli --bin substrate-node --features runtime-benchmarks -- benchmark pallet --execution wasm --wasm-execution compiled --chain dev --pallet "*" --extrinsic "*" --steps 2 --repeat 1` --- substrate/frame/alliance/src/benchmarking.rs | 567 ++++++++++-------- .../procedural/src/pallet/expand/warnings.rs | 8 +- 2 files changed, 315 insertions(+), 260 deletions(-) diff --git a/substrate/frame/alliance/src/benchmarking.rs b/substrate/frame/alliance/src/benchmarking.rs index eb32c6c466c91..77294c6b6646f 100644 --- a/substrate/frame/alliance/src/benchmarking.rs +++ b/substrate/frame/alliance/src/benchmarking.rs @@ -17,6 +17,8 @@ //! Alliance pallet benchmarking. +#![cfg(feature = "runtime-benchmarks")] + use sp_runtime::traits::{Bounded, Hash, StaticLookup}; use sp_std::{ cmp, @@ -25,7 +27,7 @@ use sp_std::{ prelude::*, }; -use frame_benchmarking::v1::{account, benchmarks_instance_pallet, BenchmarkError}; +use frame_benchmarking::{account, impl_benchmark_test_suite, v2::*, BenchmarkError}; use frame_support::traits::{EnsureOrigin, Get, UnfilteredDispatchable}; use frame_system::{pallet_prelude::BlockNumberFor, Pallet as System, RawOrigin as SystemOrigin}; @@ -94,32 +96,31 @@ fn set_members<T: Config<I>, I: 'static>() { T::InitializeMembers::initialize_members(&[fellows.as_slice()].concat()); } -benchmarks_instance_pallet! { - // This tests when proposal is created and queued as "proposed" - propose_proposed { - let b in 1 .. MAX_BYTES; - let m in 2 .. T::MaxFellows::get(); - let p in 1 .. T::MaxProposals::get(); +#[instance_benchmarks] +mod benchmarks { + use super::*; + // This tests when proposal is created and queued as "proposed" + #[benchmark] + fn propose_proposed( + b: Linear<1, MAX_BYTES>, + m: Linear<2, { T::MaxFellows::get() }>, + p: Linear<1, { T::MaxProposals::get() }>, + ) -> Result<(), BenchmarkError> { let bytes_in_storage = b + size_of::<Cid>() as u32 + 32; // Construct `members`. - let fellows = (0 .. m).map(fellow::<T, I>).collect::<Vec<_>>(); + let fellows = (0..m).map(fellow::<T, I>).collect::<Vec<_>>(); let proposer = fellows[0].clone(); - Alliance::<T, I>::init_members( - SystemOrigin::Root.into(), - fellows, - vec![], - )?; + Alliance::<T, I>::init_members(SystemOrigin::Root.into(), fellows, vec![])?; let threshold = m; // Add previous proposals. - for i in 0 .. p - 1 { + for i in 0..p - 1 { // Proposals should be different so that different proposal hashes are generated - let proposal: T::Proposal = AllianceCall::<T, I>::set_rule { - rule: rule(vec![i as u8; b as usize]) - }.into(); + let proposal: T::Proposal = + AllianceCall::<T, I>::set_rule { rule: rule(vec![i as u8; b as usize]) }.into(); Alliance::<T, I>::propose( SystemOrigin::Signed(proposer.clone()).into(), threshold, @@ -128,45 +129,45 @@ benchmarks_instance_pallet! { )?; } - let proposal: T::Proposal = AllianceCall::<T, I>::set_rule { rule: rule(vec![p as u8; b as usize]) }.into(); + let proposal: T::Proposal = + AllianceCall::<T, I>::set_rule { rule: rule(vec![p as u8; b as usize]) }.into(); + + #[extrinsic_call] + propose( + SystemOrigin::Signed(proposer.clone()), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + ); - }: propose(SystemOrigin::Signed(proposer.clone()), threshold, Box::new(proposal.clone()), bytes_in_storage) - verify { - // New proposal is recorded let proposal_hash = T::Hashing::hash_of(&proposal); assert_eq!(T::ProposalProvider::proposal_of(proposal_hash), Some(proposal)); + Ok(()) } - vote { - // We choose 5 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) - let m in 5 .. T::MaxFellows::get(); - + #[benchmark] + fn vote(m: Linear<5, { T::MaxFellows::get() }>) -> Result<(), BenchmarkError> { let p = T::MaxProposals::get(); let b = MAX_BYTES; - let bytes_in_storage = b + size_of::<Cid>() as u32 + 32; + let _bytes_in_storage = b + size_of::<Cid>() as u32 + 32; // Construct `members`. - let fellows = (0 .. m).map(fellow::<T, I>).collect::<Vec<_>>(); + let fellows = (0..m).map(fellow::<T, I>).collect::<Vec<_>>(); let proposer = fellows[0].clone(); let members = fellows.clone(); - Alliance::<T, I>::init_members( - SystemOrigin::Root.into(), - fellows, - vec![], - )?; + Alliance::<T, I>::init_members(SystemOrigin::Root.into(), fellows, vec![])?; // Threshold is 1 less than the number of members so that one person can vote nay let threshold = m - 1; // Add previous proposals let mut last_hash = T::Hash::default(); - for i in 0 .. p { + for i in 0..p { // Proposals should be different so that different proposal hashes are generated - let proposal: T::Proposal = AllianceCall::<T, I>::set_rule { - rule: rule(vec![i as u8; b as usize]) - }.into(); + let proposal: T::Proposal = + AllianceCall::<T, I>::set_rule { rule: rule(vec![i as u8; b as usize]) }.into(); Alliance::<T, I>::propose( SystemOrigin::Signed(proposer.clone()).into(), threshold, @@ -178,7 +179,7 @@ benchmarks_instance_pallet! { let index = p - 1; // Have almost everyone vote aye on last proposal, while keeping it from passing. - for j in 0 .. m - 3 { + for j in 0..m - 3 { let voter = &members[j as usize]; Alliance::<T, I>::vote( SystemOrigin::Signed(voter.clone()).into(), @@ -203,28 +204,28 @@ benchmarks_instance_pallet! { // Whitelist voter account from further DB operations. let voter_key = frame_system::Account::<T>::hashed_key_for(&voter); frame_benchmarking::benchmarking::add_to_whitelist(voter_key.into()); - }: _(SystemOrigin::Signed(voter), last_hash.clone(), index, approve) - verify { - } - close_early_disapproved { - // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) - let m in 4 .. T::MaxFellows::get(); - let p in 1 .. T::MaxProposals::get(); + #[extrinsic_call] + _(SystemOrigin::Signed(voter), last_hash.clone(), index, approve); + //nothing to verify + Ok(()) + } + + #[benchmark] + fn close_early_disapproved( + m: Linear<4, { T::MaxFellows::get() }>, + p: Linear<1, { T::MaxProposals::get() }>, + ) -> Result<(), BenchmarkError> { let bytes = 100; let bytes_in_storage = bytes + size_of::<Cid>() as u32 + 32; // Construct `members`. - let fellows = (0 .. m).map(fellow::<T, I>).collect::<Vec<_>>(); + let fellows = (0..m).map(fellow::<T, I>).collect::<Vec<_>>(); let members = fellows.clone(); - Alliance::<T, I>::init_members( - SystemOrigin::Root.into(), - fellows, - vec![], - )?; + Alliance::<T, I>::init_members(SystemOrigin::Root.into(), fellows, vec![])?; let proposer = members[0].clone(); let voter = members[1].clone(); @@ -234,11 +235,10 @@ benchmarks_instance_pallet! { // Add previous proposals let mut last_hash = T::Hash::default(); - for i in 0 .. p { + for i in 0..p { // Proposals should be different so that different proposal hashes are generated - let proposal: T::Proposal = AllianceCall::<T, I>::set_rule { - rule: rule(vec![i as u8; bytes as usize]) - }.into(); + let proposal: T::Proposal = + AllianceCall::<T, I>::set_rule { rule: rule(vec![i as u8; bytes as usize]) }.into(); Alliance::<T, I>::propose( SystemOrigin::Signed(proposer.clone()).into(), threshold, @@ -251,7 +251,7 @@ benchmarks_instance_pallet! { let index = p - 1; // Have most everyone vote aye on last proposal, while keeping it from passing. - for j in 2 .. m - 1 { + for j in 2..m - 1 { let voter = &members[j as usize]; Alliance::<T, I>::vote( SystemOrigin::Signed(voter.clone()).into(), @@ -280,44 +280,41 @@ benchmarks_instance_pallet! { // Whitelist voter account from further DB operations. let voter_key = frame_system::Account::<T>::hashed_key_for(&voter); frame_benchmarking::benchmarking::add_to_whitelist(voter_key.into()); - }: close(SystemOrigin::Signed(voter), last_hash.clone(), index, Weight::MAX, bytes_in_storage) - verify { - // The last proposal is removed. + + #[extrinsic_call] + close(SystemOrigin::Signed(voter), last_hash.clone(), index, Weight::MAX, bytes_in_storage); + assert_eq!(T::ProposalProvider::proposal_of(last_hash), None); + Ok(()) } - close_early_approved { - let b in 1 .. MAX_BYTES; - // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) - let m in 4 .. T::MaxFellows::get(); - let p in 1 .. T::MaxProposals::get(); - + // We choose 4 as a minimum for param m, so we always trigger a vote in the voting loop (`for j + // in ...`) + #[benchmark] + fn close_early_approved( + b: Linear<1, MAX_BYTES>, + m: Linear<4, { T::MaxFellows::get() }>, + p: Linear<1, { T::MaxProposals::get() }>, + ) -> Result<(), BenchmarkError> { let bytes_in_storage = b + size_of::<Cid>() as u32 + 32; // Construct `members`. - let fellows = (0 .. m).map(fellow::<T, I>).collect::<Vec<_>>(); + let fellows = (0..m).map(fellow::<T, I>).collect::<Vec<_>>(); let members = fellows.clone(); - Alliance::<T, I>::init_members( - SystemOrigin::Root.into(), - fellows, - vec![], - )?; + Alliance::<T, I>::init_members(SystemOrigin::Root.into(), fellows, vec![])?; let proposer = members[0].clone(); - let voter = members[1].clone(); - // Threshold is 2 so any two ayes will approve the vote let threshold = 2; // Add previous proposals let mut last_hash = T::Hash::default(); - for i in 0 .. p { + for i in 0..p { // Proposals should be different so that different proposal hashes are generated - let proposal: T::Proposal = AllianceCall::<T, I>::set_rule { - rule: rule(vec![i as u8; b as usize]) - }.into(); + let proposal: T::Proposal = + AllianceCall::<T, I>::set_rule { rule: rule(vec![i as u8; b as usize]) }.into(); Alliance::<T, I>::propose( SystemOrigin::Signed(proposer.clone()).into(), threshold, @@ -329,7 +326,8 @@ benchmarks_instance_pallet! { } let index = p - 1; - // Caller switches vote to nay on their own proposal, allowing them to be the deciding approval vote + // Caller switches vote to nay on their own proposal, allowing them to be the deciding + // approval vote Alliance::<T, I>::vote( SystemOrigin::Signed(proposer.clone()).into(), last_hash.clone(), @@ -338,7 +336,7 @@ benchmarks_instance_pallet! { )?; // Have almost everyone vote nay on last proposal, while keeping it from failing. - for j in 2 .. m - 1 { + for j in 2..m - 1 { let voter = &members[j as usize]; Alliance::<T, I>::vote( SystemOrigin::Signed(voter.clone()).into(), @@ -364,30 +362,28 @@ benchmarks_instance_pallet! { index, true, )?; - }: close(SystemOrigin::Signed(voter), last_hash.clone(), index, Weight::MAX, bytes_in_storage) - verify { - // The last proposal is removed. + + #[extrinsic_call] + close(SystemOrigin::Signed(voter), last_hash.clone(), index, Weight::MAX, bytes_in_storage); + assert_eq!(T::ProposalProvider::proposal_of(last_hash), None); + Ok(()) } - close_disapproved { - // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) - let m in 2 .. T::MaxFellows::get(); - let p in 1 .. T::MaxProposals::get(); - + #[benchmark] + fn close_disapproved( + m: Linear<2, { T::MaxFellows::get() }>, + p: Linear<1, { T::MaxProposals::get() }>, + ) -> Result<(), BenchmarkError> { let bytes = 100; let bytes_in_storage = bytes + size_of::<Cid>() as u32 + 32; // Construct `members`. - let fellows = (0 .. m).map(fellow::<T, I>).collect::<Vec<_>>(); + let fellows = (0..m).map(fellow::<T, I>).collect::<Vec<_>>(); let members = fellows.clone(); - Alliance::<T, I>::init_members( - SystemOrigin::Root.into(), - fellows, - vec![], - )?; + Alliance::<T, I>::init_members(SystemOrigin::Root.into(), fellows, vec![])?; let proposer = members[0].clone(); let voter = members[1].clone(); @@ -397,11 +393,10 @@ benchmarks_instance_pallet! { // Add proposals let mut last_hash = T::Hash::default(); - for i in 0 .. p { + for i in 0..p { // Proposals should be different so that different proposal hashes are generated - let proposal: T::Proposal = AllianceCall::<T, I>::set_rule { - rule: rule(vec![i as u8; bytes as usize]) - }.into(); + let proposal: T::Proposal = + AllianceCall::<T, I>::set_rule { rule: rule(vec![i as u8; bytes as usize]) }.into(); Alliance::<T, I>::propose( SystemOrigin::Signed(proposer.clone()).into(), threshold, @@ -415,7 +410,7 @@ benchmarks_instance_pallet! { let index = p - 1; // Have almost everyone vote aye on last proposal, while keeping it from passing. // A few abstainers will be the nay votes needed to fail the vote. - for j in 2 .. m - 1 { + for j in 2..m - 1 { let voter = &members[j as usize]; Alliance::<T, I>::vote( SystemOrigin::Signed(voter.clone()).into(), @@ -434,44 +429,41 @@ benchmarks_instance_pallet! { System::<T>::set_block_number(BlockNumberFor::<T>::max_value()); - }: close(SystemOrigin::Signed(voter), last_hash.clone(), index, Weight::MAX, bytes_in_storage) - verify { + #[extrinsic_call] + close(SystemOrigin::Signed(voter), last_hash.clone(), index, Weight::MAX, bytes_in_storage); + // The last proposal is removed. assert_eq!(T::ProposalProvider::proposal_of(last_hash), None); + Ok(()) } - close_approved { - let b in 1 .. MAX_BYTES; - // We choose 4 fellows as a minimum so we always trigger a vote in the voting loop (`for j in ...`) - let m in 5 .. T::MaxFellows::get(); - let p in 1 .. T::MaxProposals::get(); - + // We choose 5 fellows as a minimum so we always trigger a vote in the voting loop (`for j in + // ...`) + #[benchmark] + fn close_approved( + b: Linear<1, MAX_BYTES>, + m: Linear<5, { T::MaxFellows::get() }>, + p: Linear<1, { T::MaxProposals::get() }>, + ) -> Result<(), BenchmarkError> { let bytes_in_storage = b + size_of::<Cid>() as u32 + 32; // Construct `members`. - let fellows = (0 .. m).map(fellow::<T, I>).collect::<Vec<_>>(); + let fellows = (0..m).map(fellow::<T, I>).collect::<Vec<_>>(); let members = fellows.clone(); - Alliance::<T, I>::init_members( - SystemOrigin::Root.into(), - fellows, - vec![], - )?; + Alliance::<T, I>::init_members(SystemOrigin::Root.into(), fellows, vec![])?; let proposer = members[0].clone(); - let voter = members[1].clone(); - // Threshold is two, so any two ayes will pass the vote let threshold = 2; // Add proposals let mut last_hash = T::Hash::default(); - for i in 0 .. p { + for i in 0..p { // Proposals should be different so that different proposal hashes are generated - let proposal: T::Proposal = AllianceCall::<T, I>::set_rule { - rule: rule(vec![i as u8; b as usize]) - }.into(); + let proposal: T::Proposal = + AllianceCall::<T, I>::set_rule { rule: rule(vec![i as u8; b as usize]) }.into(); Alliance::<T, I>::propose( SystemOrigin::Signed(proposer.clone()).into(), threshold, @@ -487,70 +479,71 @@ benchmarks_instance_pallet! { SystemOrigin::Signed(proposer.clone()).into(), last_hash.clone(), p - 1, - true // Vote aye. + true, // Vote aye. )?; let index = p - 1; // Have almost everyone vote nay on last proposal, while keeping it from failing. // A few abstainers will be the aye votes needed to pass the vote. - for j in 2 .. m - 1 { + for j in 2..m - 1 { let voter = &members[j as usize]; Alliance::<T, I>::vote( SystemOrigin::Signed(voter.clone()).into(), last_hash.clone(), index, - false + false, )?; } // caller is prime, prime already votes aye by creating the proposal System::<T>::set_block_number(BlockNumberFor::<T>::max_value()); - }: close(SystemOrigin::Signed(voter), last_hash.clone(), index, Weight::MAX, bytes_in_storage) - verify { - // The last proposal is removed. + #[extrinsic_call] + close( + SystemOrigin::Signed(proposer), + last_hash.clone(), + index, + Weight::MAX, + bytes_in_storage, + ); + assert_eq!(T::ProposalProvider::proposal_of(last_hash), None); + Ok(()) } - init_members { - // at least 1 fellow - let m in 1 .. T::MaxFellows::get(); - let z in 0 .. T::MaxAllies::get(); + #[benchmark] + fn init_members( + m: Linear<1, { T::MaxFellows::get() }>, + z: Linear<0, { T::MaxAllies::get() }>, + ) -> Result<(), BenchmarkError> { + let mut fellows = (0..m).map(fellow::<T, I>).collect::<Vec<_>>(); + let mut allies = (0..z).map(ally::<T, I>).collect::<Vec<_>>(); - let mut fellows = (0 .. m).map(fellow::<T, I>).collect::<Vec<_>>(); - let mut allies = (0 .. z).map(ally::<T, I>).collect::<Vec<_>>(); + #[extrinsic_call] + _(SystemOrigin::Root, fellows.clone(), allies.clone()); - }: _(SystemOrigin::Root, fellows.clone(), allies.clone()) - verify { fellows.sort(); allies.sort(); - assert_last_event::<T, I>(Event::MembersInitialized { - fellows: fellows.clone(), - allies: allies.clone(), - }.into()); + assert_last_event::<T, I>( + Event::MembersInitialized { fellows: fellows.clone(), allies: allies.clone() }.into(), + ); assert_eq!(Alliance::<T, I>::members(MemberRole::Fellow), fellows); assert_eq!(Alliance::<T, I>::members(MemberRole::Ally), allies); + Ok(()) } - disband { - // at least 1 founders - let x in 1 .. T::MaxFellows::get(); - let y in 0 .. T::MaxAllies::get(); - let z in 0 .. T::MaxMembersCount::get() / 2; - - let fellows = (0 .. x).map(fellow::<T, I>).collect::<Vec<_>>(); - let allies = (0 .. y).map(ally::<T, I>).collect::<Vec<_>>(); - let witness = DisbandWitness{ - fellow_members: x, - ally_members: y, - }; + #[benchmark] + fn disband( + x: Linear<1, { T::MaxFellows::get() }>, + y: Linear<0, { T::MaxAllies::get() }>, + z: Linear<0, { T::MaxMembersCount::get() / 2 }>, + ) -> Result<(), BenchmarkError> { + let fellows = (0..x).map(fellow::<T, I>).collect::<Vec<_>>(); + let allies = (0..y).map(ally::<T, I>).collect::<Vec<_>>(); + let witness = DisbandWitness { fellow_members: x, ally_members: y }; // setting the Alliance to disband on the benchmark call - Alliance::<T, I>::init_members( - SystemOrigin::Root.into(), - fellows.clone(), - allies.clone(), - )?; + Alliance::<T, I>::init_members(SystemOrigin::Root.into(), fellows.clone(), allies.clone())?; // reserve deposits let deposit = T::AllyDeposit::get(); @@ -561,18 +554,25 @@ benchmarks_instance_pallet! { assert_eq!(Alliance::<T, I>::voting_members_count(), x); assert_eq!(Alliance::<T, I>::ally_members_count(), y); - }: _(SystemOrigin::Root, witness) - verify { - assert_last_event::<T, I>(Event::AllianceDisbanded { - fellow_members: x, - ally_members: y, - unreserved: cmp::min(z, x + y), - }.into()); + + #[extrinsic_call] + _(SystemOrigin::Root, witness); + + assert_last_event::<T, I>( + Event::AllianceDisbanded { + fellow_members: x, + ally_members: y, + unreserved: cmp::min(z, x + y), + } + .into(), + ); assert!(!Alliance::<T, I>::is_initialized()); + Ok(()) } - set_rule { + #[benchmark] + fn set_rule() -> Result<(), BenchmarkError> { set_members::<T, I>(); let rule = rule(b"hello world"); @@ -580,61 +580,86 @@ benchmarks_instance_pallet! { let call = Call::<T, I>::set_rule { rule: rule.clone() }; let origin = T::AdminOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; - }: { call.dispatch_bypass_filter(origin)? } - verify { + + #[block] + { + call.dispatch_bypass_filter(origin)?; + } assert_eq!(Alliance::<T, I>::rule(), Some(rule.clone())); assert_last_event::<T, I>(Event::NewRuleSet { rule }.into()); + Ok(()) } - announce { + #[benchmark] + fn announce() -> Result<(), BenchmarkError> { set_members::<T, I>(); let announcement = announcement(b"hello world"); let call = Call::<T, I>::announce { announcement: announcement.clone() }; - let origin = - T::AnnouncementOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; - }: { call.dispatch_bypass_filter(origin)? } - verify { + let origin = T::AnnouncementOrigin::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + + #[block] + { + call.dispatch_bypass_filter(origin)?; + } + assert!(Alliance::<T, I>::announcements().contains(&announcement)); assert_last_event::<T, I>(Event::Announced { announcement }.into()); + Ok(()) } - remove_announcement { + #[benchmark] + fn remove_announcement() -> Result<(), BenchmarkError> { set_members::<T, I>(); let announcement = announcement(b"hello world"); - let announcements: BoundedVec<_, T::MaxAnnouncementsCount> = BoundedVec::try_from(vec![announcement.clone()]).unwrap(); + let announcements: BoundedVec<_, T::MaxAnnouncementsCount> = + BoundedVec::try_from(vec![announcement.clone()]).unwrap(); Announcements::<T, I>::put(announcements); let call = Call::<T, I>::remove_announcement { announcement: announcement.clone() }; - let origin = - T::AnnouncementOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; - }: { call.dispatch_bypass_filter(origin)? } - verify { - assert!(Alliance::<T, I>::announcements().is_empty()); + let origin = T::AnnouncementOrigin::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + + #[block] + { + call.dispatch_bypass_filter(origin)?; + } + + assert!(!Alliance::<T, I>::announcements().contains(&announcement)); assert_last_event::<T, I>(Event::AnnouncementRemoved { announcement }.into()); + Ok(()) } - join_alliance { + #[benchmark] + fn join_alliance() -> Result<(), BenchmarkError> { set_members::<T, I>(); let outsider = outsider::<T, I>(1); assert!(!Alliance::<T, I>::is_member(&outsider)); assert_eq!(DepositOf::<T, I>::get(&outsider), None); - }: _(SystemOrigin::Signed(outsider.clone())) - verify { + + #[extrinsic_call] + _(SystemOrigin::Signed(outsider.clone())); + assert!(Alliance::<T, I>::is_member_of(&outsider, MemberRole::Ally)); // outsider is now an ally assert_eq!(DepositOf::<T, I>::get(&outsider), Some(T::AllyDeposit::get())); // with a deposit assert!(!Alliance::<T, I>::has_voting_rights(&outsider)); // allies don't have voting rights - assert_last_event::<T, I>(Event::NewAllyJoined { - ally: outsider, - nominator: None, - reserved: Some(T::AllyDeposit::get()) - }.into()); + assert_last_event::<T, I>( + Event::NewAllyJoined { + ally: outsider, + nominator: None, + reserved: Some(T::AllyDeposit::get()), + } + .into(), + ); + Ok(()) } - nominate_ally { + #[benchmark] + fn nominate_ally() -> Result<(), BenchmarkError> { set_members::<T, I>(); let fellow1 = fellow::<T, I>(1); @@ -645,19 +670,23 @@ benchmarks_instance_pallet! { assert_eq!(DepositOf::<T, I>::get(&outsider), None); let outsider_lookup = T::Lookup::unlookup(outsider.clone()); - }: _(SystemOrigin::Signed(fellow1.clone()), outsider_lookup) - verify { + + #[extrinsic_call] + _(SystemOrigin::Signed(fellow1.clone()), outsider_lookup); + assert!(Alliance::<T, I>::is_member_of(&outsider, MemberRole::Ally)); // outsider is now an ally assert_eq!(DepositOf::<T, I>::get(&outsider), None); // without a deposit assert!(!Alliance::<T, I>::has_voting_rights(&outsider)); // allies don't have voting rights - assert_last_event::<T, I>(Event::NewAllyJoined { - ally: outsider, - nominator: Some(fellow1), - reserved: None - }.into()); + assert_last_event::<T, I>( + Event::NewAllyJoined { ally: outsider, nominator: Some(fellow1), reserved: None } + .into(), + ); + + Ok(()) } - elevate_ally { + #[benchmark] + fn elevate_ally() -> Result<(), BenchmarkError> { set_members::<T, I>(); let ally1 = ally::<T, I>(1); @@ -665,59 +694,69 @@ benchmarks_instance_pallet! { let ally1_lookup = T::Lookup::unlookup(ally1.clone()); let call = Call::<T, I>::elevate_ally { ally: ally1_lookup }; - let origin = - T::MembershipManager::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; - }: { call.dispatch_bypass_filter(origin)? } - verify { + let origin = T::MembershipManager::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + + #[block] + { + call.dispatch_bypass_filter(origin)?; + } + assert!(!Alliance::<T, I>::is_ally(&ally1)); assert!(Alliance::<T, I>::has_voting_rights(&ally1)); assert_last_event::<T, I>(Event::AllyElevated { ally: ally1 }.into()); + Ok(()) } - give_retirement_notice { + #[benchmark] + fn give_retirement_notice() -> Result<(), BenchmarkError> { set_members::<T, I>(); let fellow2 = fellow::<T, I>(2); assert!(Alliance::<T, I>::has_voting_rights(&fellow2)); - }: _(SystemOrigin::Signed(fellow2.clone())) - verify { + + #[extrinsic_call] + _(SystemOrigin::Signed(fellow2.clone())); + assert!(Alliance::<T, I>::is_member_of(&fellow2, MemberRole::Retiring)); assert_eq!( RetiringMembers::<T, I>::get(&fellow2), Some(System::<T>::block_number() + T::RetirementPeriod::get()) ); - assert_last_event::<T, I>( - Event::MemberRetirementPeriodStarted {member: fellow2}.into() - ); + assert_last_event::<T, I>(Event::MemberRetirementPeriodStarted { member: fellow2 }.into()); + Ok(()) } - retire { + #[benchmark] + fn retire() -> Result<(), BenchmarkError> { set_members::<T, I>(); let fellow2 = fellow::<T, I>(2); assert!(Alliance::<T, I>::has_voting_rights(&fellow2)); assert_eq!( - Alliance::<T, I>::give_retirement_notice( - SystemOrigin::Signed(fellow2.clone()).into() - ), + Alliance::<T, I>::give_retirement_notice(SystemOrigin::Signed(fellow2.clone()).into()), Ok(()) ); System::<T>::set_block_number(System::<T>::block_number() + T::RetirementPeriod::get()); assert_eq!(DepositOf::<T, I>::get(&fellow2), Some(T::AllyDeposit::get())); - }: _(SystemOrigin::Signed(fellow2.clone())) - verify { + + #[extrinsic_call] + _(SystemOrigin::Signed(fellow2.clone())); + assert!(!Alliance::<T, I>::is_member(&fellow2)); assert_eq!(DepositOf::<T, I>::get(&fellow2), None); - assert_last_event::<T, I>(Event::MemberRetired { - member: fellow2, - unreserved: Some(T::AllyDeposit::get()) - }.into()); + assert_last_event::<T, I>( + Event::MemberRetired { member: fellow2, unreserved: Some(T::AllyDeposit::get()) } + .into(), + ); + Ok(()) } - kick_member { + #[benchmark] + fn kick_member() -> Result<(), BenchmarkError> { set_members::<T, I>(); let fellow2 = fellow::<T, I>(2); @@ -726,58 +765,71 @@ benchmarks_instance_pallet! { let fellow2_lookup = T::Lookup::unlookup(fellow2.clone()); let call = Call::<T, I>::kick_member { who: fellow2_lookup }; - let origin = - T::MembershipManager::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; - }: { call.dispatch_bypass_filter(origin)? } - verify { + let origin = T::MembershipManager::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + + #[block] + { + call.dispatch_bypass_filter(origin)?; + } + assert!(!Alliance::<T, I>::is_member(&fellow2)); assert_eq!(DepositOf::<T, I>::get(&fellow2), None); - assert_last_event::<T, I>(Event::MemberKicked { - member: fellow2, - slashed: Some(T::AllyDeposit::get()) - }.into()); + assert_last_event::<T, I>( + Event::MemberKicked { member: fellow2, slashed: Some(T::AllyDeposit::get()) }.into(), + ); + Ok(()) } - add_unscrupulous_items { - let n in 0 .. T::MaxUnscrupulousItems::get(); - let l in 0 .. T::MaxWebsiteUrlLength::get(); - + #[benchmark] + fn add_scrupulous_items( + n: Linear<0, { T::MaxUnscrupulousItems::get() }>, + l: Linear<0, { T::MaxWebsiteUrlLength::get() }>, + ) -> Result<(), BenchmarkError> { set_members::<T, I>(); - let accounts = (0 .. n) - .map(|i| generate_unscrupulous_account::<T, I>(i)) + let accounts = (0..n).map(|i| generate_unscrupulous_account::<T, I>(i)).collect::<Vec<_>>(); + let websites = (0..n) + .map(|i| -> BoundedVec<u8, T::MaxWebsiteUrlLength> { + BoundedVec::try_from(vec![i as u8; l as usize]).unwrap() + }) .collect::<Vec<_>>(); - let websites = (0 .. n).map(|i| -> BoundedVec<u8, T::MaxWebsiteUrlLength> { - BoundedVec::try_from(vec![i as u8; l as usize]).unwrap() - }).collect::<Vec<_>>(); let mut unscrupulous_list = Vec::with_capacity(accounts.len() + websites.len()); unscrupulous_list.extend(accounts.into_iter().map(UnscrupulousItem::AccountId)); unscrupulous_list.extend(websites.into_iter().map(UnscrupulousItem::Website)); let call = Call::<T, I>::add_unscrupulous_items { items: unscrupulous_list.clone() }; - let origin = - T::AnnouncementOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; - }: { call.dispatch_bypass_filter(origin)? } - verify { + let origin = T::AnnouncementOrigin::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + + #[block] + { + call.dispatch_bypass_filter(origin)?; + } + assert_last_event::<T, I>(Event::UnscrupulousItemAdded { items: unscrupulous_list }.into()); + Ok(()) } - remove_unscrupulous_items { - let n in 0 .. T::MaxUnscrupulousItems::get(); - let l in 0 .. T::MaxWebsiteUrlLength::get(); - + #[benchmark] + fn remove_unscrupulous_items( + n: Linear<0, { T::MaxUnscrupulousItems::get() }>, + l: Linear<0, { T::MaxWebsiteUrlLength::get() }>, + ) -> Result<(), BenchmarkError> { set_members::<T, I>(); - let mut accounts = (0 .. n) - .map(|i| generate_unscrupulous_account::<T, I>(i)) - .collect::<Vec<_>>(); + let mut accounts = + (0..n).map(|i| generate_unscrupulous_account::<T, I>(i)).collect::<Vec<_>>(); accounts.sort(); let accounts: BoundedVec<_, T::MaxUnscrupulousItems> = accounts.try_into().unwrap(); UnscrupulousAccounts::<T, I>::put(accounts.clone()); - let mut websites = (0 .. n).map(|i| -> BoundedVec<_, T::MaxWebsiteUrlLength> - { BoundedVec::try_from(vec![i as u8; l as usize]).unwrap() }).collect::<Vec<_>>(); + let mut websites = (0..n) + .map(|i| -> BoundedVec<_, T::MaxWebsiteUrlLength> { + BoundedVec::try_from(vec![i as u8; l as usize]).unwrap() + }) + .collect::<Vec<_>>(); websites.sort(); let websites: BoundedVec<_, T::MaxUnscrupulousItems> = websites.try_into().unwrap(); UnscrupulousWebsites::<T, I>::put(websites.clone()); @@ -787,24 +839,31 @@ benchmarks_instance_pallet! { unscrupulous_list.extend(websites.into_iter().map(UnscrupulousItem::Website)); let call = Call::<T, I>::remove_unscrupulous_items { items: unscrupulous_list.clone() }; - let origin = - T::AnnouncementOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; - }: { call.dispatch_bypass_filter(origin)? } - verify { - assert_last_event::<T, I>(Event::UnscrupulousItemRemoved { items: unscrupulous_list }.into()); + let origin = T::AnnouncementOrigin::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + + #[block] + { + call.dispatch_bypass_filter(origin)?; + } + + assert_last_event::<T, I>( + Event::UnscrupulousItemRemoved { items: unscrupulous_list }.into(), + ); + Ok(()) } - abdicate_fellow_status { + #[benchmark] + fn abdicate_fellow_status() -> Result<(), BenchmarkError> { set_members::<T, I>(); let fellow2 = fellow::<T, I>(2); assert!(Alliance::<T, I>::has_voting_rights(&fellow2)); - }: _(SystemOrigin::Signed(fellow2.clone())) - verify { - assert!(Alliance::<T, I>::is_member_of(&fellow2, MemberRole::Ally)); - assert_last_event::<T, I>( - Event::FellowAbdicated {fellow: fellow2}.into() - ); + #[extrinsic_call] + _(SystemOrigin::Signed(fellow2.clone())); + + assert_last_event::<T, I>(Event::FellowAbdicated { fellow: fellow2 }.into()); + Ok(()) } impl_benchmark_test_suite!(Alliance, crate::mock::new_bench_ext(), crate::mock::Test); diff --git a/substrate/frame/support/procedural/src/pallet/expand/warnings.rs b/substrate/frame/support/procedural/src/pallet/expand/warnings.rs index ae5890878a2f6..030e3ddaf3232 100644 --- a/substrate/frame/support/procedural/src/pallet/expand/warnings.rs +++ b/substrate/frame/support/procedural/src/pallet/expand/warnings.rs @@ -33,9 +33,7 @@ pub(crate) fn weight_witness_warning( if dev_mode { return } - let CallWeightDef::Immediate(w) = &method.weight else { - return; - }; + let CallWeightDef::Immediate(w) = &method.weight else { return }; let partial_warning = Warning::new_deprecated("UncheckedWeightWitness") .old("not check weight witness data") @@ -66,9 +64,7 @@ pub(crate) fn weight_constant_warning( if dev_mode { return } - let syn::Expr::Lit(lit) = weight else { - return; - }; + let syn::Expr::Lit(lit) = weight else { return }; let warning = Warning::new_deprecated("ConstantWeight") .index(warnings.len()) From 1f28cddd6fcaeb54220d4c410abe80fde826fc75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=B3nal=20Murray?= <donal.murray@parity.io> Date: Fri, 13 Oct 2023 23:06:38 +0200 Subject: [PATCH 43/54] Remove clippy clone-double-ref lint noise (#1860) The lint `clippy::clone_double_ref` was renamed to `suspicious_double_ref_op` in [this commit](https://github.com/rust-lang/rust/commit/5c99175a9efcaa3d65712c119f361add22e3a859) (thanks @liamaharon) and now generates a lot of noise in the terminal when run both in CI and locally with 1.73. This renames the lint in line with this, but does not change functionality. May cause issues for people running earlier versions locally - @altaua requesting review to get your opinion on this. --- .cargo/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 042dded2fa9b3..a5ff1d8dffa08 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -15,7 +15,7 @@ rustflags = [ "-Aclippy::all", "-Dclippy::correctness", "-Aclippy::if-same-then-else", - "-Aclippy::clone-double-ref", + "-Asuspicious_double_ref_op", "-Dclippy::complexity", "-Aclippy::zero-prefixed-literal", # 00_1000_000 "-Aclippy::type_complexity", # raison d'etre From 7c87d61f5a8e979a200d3f2676a5c83e35c31db4 Mon Sep 17 00:00:00 2001 From: Juan <juangirini@gmail.com> Date: Sat, 14 Oct 2023 08:26:19 +0200 Subject: [PATCH 44/54] Macros to use path instead of ident (#1474) --- Cargo.lock | 17 ++ Cargo.toml | 2 + .../frame/support/procedural/src/benchmark.rs | 29 +-- .../procedural/src/construct_runtime/mod.rs | 14 +- .../support/procedural/src/crate_version.rs | 4 +- .../support/procedural/src/key_prefix.rs | 7 +- substrate/frame/support/procedural/src/lib.rs | 10 +- .../procedural/src/pallet/expand/error.rs | 8 +- .../src/pallet/expand/genesis_config.rs | 3 +- .../src/pallet/expand/tt_default_parts.rs | 16 +- .../procedural/src/pallet/parse/composite.rs | 2 +- .../procedural/src/pallet/parse/config.rs | 181 ++++++++++++++---- .../procedural/src/pallet/parse/mod.rs | 10 +- .../support/procedural/src/pallet_error.rs | 8 +- .../support/procedural/src/storage_alias.rs | 8 +- .../support/procedural/src/transactional.rs | 6 +- .../frame/support/procedural/src/tt_macro.rs | 13 +- .../frame/support/procedural/tools/src/lib.rs | 90 ++++++--- .../support/test/compile_pass/Cargo.toml | 4 +- .../support/test/compile_pass/src/lib.rs | 7 +- .../support/test/stg_frame_crate/Cargo.toml | 21 ++ .../test/stg_frame_crate/frame/Cargo.toml | 20 ++ .../test/stg_frame_crate/frame/src/lib.rs | 21 ++ .../support/test/stg_frame_crate/src/lib.rs | 75 ++++++++ .../pallet_ui/event_type_invalid_bound.rs | 3 +- ...ent_type_bound_system_config_assoc_type.rs | 44 +++++ 26 files changed, 489 insertions(+), 134 deletions(-) create mode 100644 substrate/frame/support/test/stg_frame_crate/Cargo.toml create mode 100644 substrate/frame/support/test/stg_frame_crate/frame/Cargo.toml create mode 100644 substrate/frame/support/test/stg_frame_crate/frame/src/lib.rs create mode 100644 substrate/frame/support/test/stg_frame_crate/src/lib.rs create mode 100644 substrate/frame/support/test/tests/pallet_ui/pass/event_type_bound_system_config_assoc_type.rs diff --git a/Cargo.lock b/Cargo.lock index 58bacc9db7399..ed5e0a29f71c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5098,6 +5098,14 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" +[[package]] +name = "frame" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", +] + [[package]] name = "frame-benchmarking" version = "4.0.0-dev" @@ -5426,6 +5434,15 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "frame-support-test-stg-frame-crate" +version = "0.1.0" +dependencies = [ + "frame", + "parity-scale-codec", + "scale-info", +] + [[package]] name = "frame-system" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 75da6681465d2..9e8ead6cf7c9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -345,6 +345,8 @@ members = [ "substrate/frame/support/test", "substrate/frame/support/test/compile_pass", "substrate/frame/support/test/pallet", + "substrate/frame/support/test/stg_frame_crate/frame", + "substrate/frame/support/test/stg_frame_crate", "substrate/frame/system", "substrate/frame/system/benchmarking", "substrate/frame/system/rpc/runtime-api", diff --git a/substrate/frame/support/procedural/src/benchmark.rs b/substrate/frame/support/procedural/src/benchmark.rs index 6f8f1d155e1e5..fb55e8c9f662c 100644 --- a/substrate/frame/support/procedural/src/benchmark.rs +++ b/substrate/frame/support/procedural/src/benchmark.rs @@ -18,7 +18,7 @@ //! Home of the parsing and expansion code for the new pallet benchmarking syntax use derive_syn_parse::Parse; -use frame_support_procedural_tools::generate_crate_access_2018; +use frame_support_procedural_tools::generate_access_from_frame_or_crate; use proc_macro::TokenStream; use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use quote::{quote, ToTokens}; @@ -418,7 +418,8 @@ pub fn benchmarks( true => quote!(T: Config<I>, I: 'static), }; - let krate = generate_crate_access_2018("frame-benchmarking")?; + let krate = generate_access_from_frame_or_crate("frame-benchmarking")?; + let frame_system = generate_access_from_frame_or_crate("frame-system")?; // benchmark name variables let benchmark_names_str: Vec<String> = benchmark_names.iter().map(|n| n.to_string()).collect(); @@ -488,7 +489,7 @@ pub fn benchmarks( } #[cfg(any(feature = "runtime-benchmarks", test))] impl<#type_impl_generics> #krate::Benchmarking for Pallet<#type_use_generics> - where T: frame_system::Config, #where_clause + where T: #frame_system::Config, #where_clause { fn benchmarks( extra: bool, @@ -535,7 +536,7 @@ pub fn benchmarks( _ => return Err("Could not find extrinsic.".into()), }; let mut whitelist = whitelist.to_vec(); - let whitelisted_caller_key = <frame_system::Account< + let whitelisted_caller_key = <#frame_system::Account< T, > as #krate::__private::storage::StorageMap<_, _,>>::hashed_key_for( #krate::whitelisted_caller::<T::AccountId>() @@ -571,8 +572,8 @@ pub fn benchmarks( >::instance(&selected_benchmark, c, verify)?; // Set the block number to at least 1 so events are deposited. - if #krate::__private::Zero::is_zero(&frame_system::Pallet::<T>::block_number()) { - frame_system::Pallet::<T>::set_block_number(1u32.into()); + if #krate::__private::Zero::is_zero(&#frame_system::Pallet::<T>::block_number()) { + #frame_system::Pallet::<T>::set_block_number(1u32.into()); } // Commit the externalities to the database, flushing the DB cache. @@ -654,7 +655,7 @@ pub fn benchmarks( } #[cfg(test)] - impl<#type_impl_generics> Pallet<#type_use_generics> where T: ::frame_system::Config, #where_clause { + impl<#type_impl_generics> Pallet<#type_use_generics> where T: #frame_system::Config, #where_clause { /// Test a particular benchmark by name. /// /// This isn't called `test_benchmark_by_name` just in case some end-user eventually @@ -719,10 +720,14 @@ fn expand_benchmark( where_clause: TokenStream2, ) -> TokenStream2 { // set up variables needed during quoting - let krate = match generate_crate_access_2018("frame-benchmarking") { + let krate = match generate_access_from_frame_or_crate("frame-benchmarking") { Ok(ident) => ident, Err(err) => return err.to_compile_error().into(), }; + let frame_system = match generate_access_from_frame_or_crate("frame-system") { + Ok(path) => path, + Err(err) => return err.to_compile_error().into(), + }; let codec = quote!(#krate::__private::codec); let traits = quote!(#krate::__private::traits); let setup_stmts = benchmark_def.setup_stmts; @@ -762,7 +767,7 @@ fn expand_benchmark( Expr::Cast(t) => { let ty = t.ty.clone(); quote! { - <<T as frame_system::Config>::RuntimeOrigin as From<#ty>>::from(#origin); + <<T as #frame_system::Config>::RuntimeOrigin as From<#ty>>::from(#origin); } }, _ => quote! { @@ -932,7 +937,7 @@ fn expand_benchmark( } #[cfg(test)] - impl<#type_impl_generics> Pallet<#type_use_generics> where T: ::frame_system::Config, #where_clause { + impl<#type_impl_generics> Pallet<#type_use_generics> where T: #frame_system::Config, #where_clause { #[allow(unused)] fn #test_ident() -> Result<(), #krate::BenchmarkError> { let selected_benchmark = SelectedBenchmark::#name; @@ -951,8 +956,8 @@ fn expand_benchmark( >::instance(&selected_benchmark, &c, true)?; // Set the block number to at least 1 so events are deposited. - if #krate::__private::Zero::is_zero(&frame_system::Pallet::<T>::block_number()) { - frame_system::Pallet::<T>::set_block_number(1u32.into()); + if #krate::__private::Zero::is_zero(&#frame_system::Pallet::<T>::block_number()) { + #frame_system::Pallet::<T>::set_block_number(1u32.into()); } // Run execution + verification diff --git a/substrate/frame/support/procedural/src/construct_runtime/mod.rs b/substrate/frame/support/procedural/src/construct_runtime/mod.rs index c3d433643fd7e..ce34694275b38 100644 --- a/substrate/frame/support/procedural/src/construct_runtime/mod.rs +++ b/substrate/frame/support/procedural/src/construct_runtime/mod.rs @@ -214,7 +214,7 @@ mod parse; use crate::pallet::parse::helper::two128_str; use cfg_expr::Predicate; use frame_support_procedural_tools::{ - generate_crate_access, generate_crate_access_2018, generate_hidden_includes, + generate_access_from_frame_or_crate, generate_crate_access, generate_hidden_includes, }; use itertools::Itertools; use parse::{ExplicitRuntimeDeclaration, ImplicitRuntimeDeclaration, Pallet, RuntimeDeclaration}; @@ -272,7 +272,7 @@ fn construct_runtime_implicit_to_explicit( input: TokenStream2, definition: ImplicitRuntimeDeclaration, ) -> Result<TokenStream2> { - let frame_support = generate_crate_access_2018("frame-support")?; + let frame_support = generate_access_from_frame_or_crate("frame-support")?; let mut expansion = quote::quote!( #frame_support::construct_runtime! { #input } ); @@ -283,7 +283,7 @@ fn construct_runtime_implicit_to_explicit( expansion = quote::quote!( #frame_support::__private::tt_call! { macro = [{ #pallet_path::tt_default_parts }] - frame_support = [{ #frame_support }] + your_tt_return = [{ #frame_support::__private::tt_return }] ~~> #frame_support::match_and_insert! { target = [{ #expansion }] pattern = [{ #pallet_name: #pallet_path #pallet_instance }] @@ -308,7 +308,7 @@ fn construct_runtime_explicit_to_explicit_expanded( input: TokenStream2, definition: ExplicitRuntimeDeclaration, ) -> Result<TokenStream2> { - let frame_support = generate_crate_access_2018("frame-support")?; + let frame_support = generate_access_from_frame_or_crate("frame-support")?; let mut expansion = quote::quote!( #frame_support::construct_runtime! { #input } ); @@ -319,7 +319,7 @@ fn construct_runtime_explicit_to_explicit_expanded( expansion = quote::quote!( #frame_support::__private::tt_call! { macro = [{ #pallet_path::tt_extra_parts }] - frame_support = [{ #frame_support }] + your_tt_return = [{ #frame_support::__private::tt_return }] ~~> #frame_support::match_and_insert! { target = [{ #expansion }] pattern = [{ #pallet_name: #pallet_path #pallet_instance }] @@ -372,7 +372,7 @@ fn construct_runtime_final_expansion( let scrate = generate_crate_access(hidden_crate_name, "frame-support"); let scrate_decl = generate_hidden_includes(hidden_crate_name, "frame-support"); - let frame_system = generate_crate_access_2018("frame-system")?; + let frame_system = generate_access_from_frame_or_crate("frame-system")?; let block = quote!(<#name as #frame_system::Config>::Block); let unchecked_extrinsic = quote!(<#block as #scrate::sp_runtime::traits::Block>::Extrinsic); @@ -799,7 +799,7 @@ fn decl_static_assertions( quote! { #scrate::__private::tt_call! { macro = [{ #path::tt_error_token }] - frame_support = [{ #scrate }] + your_tt_return = [{ #scrate::__private::tt_return }] ~~> #scrate::assert_error_encoded_size! { path = [{ #path }] runtime = [{ #runtime }] diff --git a/substrate/frame/support/procedural/src/crate_version.rs b/substrate/frame/support/procedural/src/crate_version.rs index 3f728abdb0b03..8c8975a425971 100644 --- a/substrate/frame/support/procedural/src/crate_version.rs +++ b/substrate/frame/support/procedural/src/crate_version.rs @@ -18,7 +18,7 @@ //! Implementation of macros related to crate versioning. use super::get_cargo_env_var; -use frame_support_procedural_tools::generate_crate_access_2018; +use frame_support_procedural_tools::generate_access_from_frame_or_crate; use proc_macro2::{Span, TokenStream}; use syn::{Error, Result}; @@ -42,7 +42,7 @@ pub fn crate_to_crate_version(input: proc_macro::TokenStream) -> Result<TokenStr let patch_version = get_cargo_env_var::<u8>("CARGO_PKG_VERSION_PATCH") .map_err(|_| create_error("Patch version needs to fit into `u8`"))?; - let crate_ = generate_crate_access_2018("frame-support")?; + let crate_ = generate_access_from_frame_or_crate("frame-support")?; Ok(quote::quote! { #crate_::traits::CrateVersion { diff --git a/substrate/frame/support/procedural/src/key_prefix.rs b/substrate/frame/support/procedural/src/key_prefix.rs index 6f793d0e37bde..7f1ab6866d1e8 100644 --- a/substrate/frame/support/procedural/src/key_prefix.rs +++ b/substrate/frame/support/procedural/src/key_prefix.rs @@ -15,6 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use frame_support_procedural_tools::generate_access_from_frame_or_crate; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; use syn::{Ident, Result}; @@ -27,6 +28,7 @@ pub fn impl_key_prefix_for_tuples(input: proc_macro::TokenStream) -> Result<Toke } let mut all_trait_impls = TokenStream::new(); + let frame_support = generate_access_from_frame_or_crate("frame-support")?; for i in 2..=MAX_IDENTS { let current_tuple = (0..i) @@ -64,7 +66,10 @@ pub fn impl_key_prefix_for_tuples(input: proc_macro::TokenStream) -> Result<Toke #(#hashers: ReversibleStorageHasher,)* #(#kargs: EncodeLike<#prefixes>),* > HasReversibleKeyPrefix<( #( #kargs, )* )> for ( #( Key<#hashers, #current_tuple>, )* ) { - fn decode_partial_key(key_material: &[u8]) -> Result<Self::Suffix, codec::Error> { + fn decode_partial_key(key_material: &[u8]) -> Result< + Self::Suffix, + #frame_support::__private::codec::Error, + > { <#suffix_keygen>::decode_final_key(key_material).map(|k| k.0) } } diff --git a/substrate/frame/support/procedural/src/lib.rs b/substrate/frame/support/procedural/src/lib.rs index 07b5a50da417b..9c551b9f23067 100644 --- a/substrate/frame/support/procedural/src/lib.rs +++ b/substrate/frame/support/procedural/src/lib.rs @@ -33,7 +33,7 @@ mod storage_alias; mod transactional; mod tt_macro; -use frame_support_procedural_tools::generate_crate_access_2018; +use frame_support_procedural_tools::generate_access_from_frame_or_crate; use macro_magic::{import_tokens_attr, import_tokens_attr_verbatim}; use proc_macro::TokenStream; use quote::{quote, ToTokens}; @@ -754,9 +754,9 @@ pub fn storage_alias(attributes: TokenStream, input: TokenStream) -> TokenStream #[import_tokens_attr_verbatim { format!( "{}::macro_magic", - match generate_crate_access_2018("frame-support") { + match generate_access_from_frame_or_crate("frame-support") { Ok(path) => Ok(path), - Err(_) => generate_crate_access_2018("frame"), + Err(_) => generate_access_from_frame_or_crate("frame"), } .expect("Failed to find either `frame-support` or `frame` in `Cargo.toml` dependencies.") .to_token_stream() @@ -1612,9 +1612,9 @@ pub fn pallet_section(attr: TokenStream, tokens: TokenStream) -> TokenStream { #[import_tokens_attr { format!( "{}::macro_magic", - match generate_crate_access_2018("frame-support") { + match generate_access_from_frame_or_crate("frame-support") { Ok(path) => Ok(path), - Err(_) => generate_crate_access_2018("frame"), + Err(_) => generate_access_from_frame_or_crate("frame"), } .expect("Failed to find either `frame-support` or `frame` in `Cargo.toml` dependencies.") .to_token_stream() diff --git a/substrate/frame/support/procedural/src/pallet/expand/error.rs b/substrate/frame/support/procedural/src/pallet/expand/error.rs index d3aa0b762bc52..877489fd6057b 100644 --- a/substrate/frame/support/procedural/src/pallet/expand/error.rs +++ b/substrate/frame/support/procedural/src/pallet/expand/error.rs @@ -42,9 +42,9 @@ pub fn expand_error(def: &mut Def) -> proc_macro2::TokenStream { macro_rules! #error_token_unique_id { { $caller:tt - frame_support = [{ $($frame_support:ident)::* }] + your_tt_return = [{ $my_tt_return:path }] } => { - $($frame_support::)*__private::tt_return! { + $my_tt_return! { $caller } }; @@ -170,9 +170,9 @@ pub fn expand_error(def: &mut Def) -> proc_macro2::TokenStream { macro_rules! #error_token_unique_id { { $caller:tt - frame_support = [{ $($frame_support:ident)::* }] + your_tt_return = [{ $my_tt_return:path }] } => { - $($frame_support::)*__private::tt_return! { + $my_tt_return! { $caller error = [{ #error_ident }] } diff --git a/substrate/frame/support/procedural/src/pallet/expand/genesis_config.rs b/substrate/frame/support/procedural/src/pallet/expand/genesis_config.rs index b00f9bcd1a662..31d519ef29293 100644 --- a/substrate/frame/support/procedural/src/pallet/expand/genesis_config.rs +++ b/substrate/frame/support/procedural/src/pallet/expand/genesis_config.rs @@ -17,6 +17,7 @@ use crate::{pallet::Def, COUNTER}; use frame_support_procedural_tools::get_doc_literals; +use quote::ToTokens; use syn::{spanned::Spanned, Ident}; /// @@ -79,7 +80,7 @@ pub fn expand_genesis_config(def: &mut Def) -> proc_macro2::TokenStream { let genesis_config_item = &mut def.item.content.as_mut().expect("Checked by def parser").1[genesis_config.index]; - let serde_crate = format!("{}::__private::serde", frame_support); + let serde_crate = format!("{}::__private::serde", frame_support.to_token_stream()); match genesis_config_item { syn::Item::Enum(syn::ItemEnum { attrs, .. }) | diff --git a/substrate/frame/support/procedural/src/pallet/expand/tt_default_parts.rs b/substrate/frame/support/procedural/src/pallet/expand/tt_default_parts.rs index 86db56c776dfe..c9a776ee24752 100644 --- a/substrate/frame/support/procedural/src/pallet/expand/tt_default_parts.rs +++ b/substrate/frame/support/procedural/src/pallet/expand/tt_default_parts.rs @@ -85,18 +85,16 @@ pub fn expand_tt_default_parts(def: &mut Def) -> proc_macro2::TokenStream { // wrapped inside of braces and finally prepended with double colons, to the caller inside // of a key named `tokens`. // - // We need to accept a frame_support argument here, because this macro gets expanded on the - // crate that called the `construct_runtime!` macro, and said crate may have renamed - // frame-support, and so we need to pass in the frame-support path that said crate - // recognizes. + // We need to accept a path argument here, because this macro gets expanded on the + // crate that called the `construct_runtime!` macro, and the actual path is unknown. #[macro_export] #[doc(hidden)] macro_rules! #default_parts_unique_id { { $caller:tt - frame_support = [{ $($frame_support:ident)::* }] + your_tt_return = [{ $my_tt_return:path }] } => { - $($frame_support)*::__private::tt_return! { + $my_tt_return! { $caller tokens = [{ expanded::{ @@ -112,7 +110,7 @@ pub fn expand_tt_default_parts(def: &mut Def) -> proc_macro2::TokenStream { pub use #default_parts_unique_id as tt_default_parts; - // This macro is similar to the `tt_default_parts!`. It expands the pallets thare are declared + // This macro is similar to the `tt_default_parts!`. It expands the pallets that are declared // explicitly (`System: frame_system::{Pallet, Call}`) with extra parts. // // For example, after expansion an explicit pallet would look like: @@ -124,9 +122,9 @@ pub fn expand_tt_default_parts(def: &mut Def) -> proc_macro2::TokenStream { macro_rules! #extra_parts_unique_id { { $caller:tt - frame_support = [{ $($frame_support:ident)::* }] + your_tt_return = [{ $my_tt_return:path }] } => { - $($frame_support)*::__private::tt_return! { + $my_tt_return! { $caller tokens = [{ expanded::{ diff --git a/substrate/frame/support/procedural/src/pallet/parse/composite.rs b/substrate/frame/support/procedural/src/pallet/parse/composite.rs index ddcc2d07f93b3..6e6ea6a795c14 100644 --- a/substrate/frame/support/procedural/src/pallet/parse/composite.rs +++ b/substrate/frame/support/procedural/src/pallet/parse/composite.rs @@ -92,7 +92,7 @@ impl CompositeDef { pub fn try_from( attr_span: proc_macro2::Span, index: usize, - scrate: &proc_macro2::Ident, + scrate: &syn::Path, item: &mut syn::Item, ) -> syn::Result<Self> { let item = if let syn::Item::Enum(item) = item { diff --git a/substrate/frame/support/procedural/src/pallet/parse/config.rs b/substrate/frame/support/procedural/src/pallet/parse/config.rs index e505f8b04119b..fbab92db196cb 100644 --- a/substrate/frame/support/procedural/src/pallet/parse/config.rs +++ b/substrate/frame/support/procedural/src/pallet/parse/config.rs @@ -16,7 +16,7 @@ // limitations under the License. use super::helper; -use frame_support_procedural_tools::get_doc_literals; +use frame_support_procedural_tools::{get_doc_literals, is_using_frame_crate}; use quote::ToTokens; use syn::{spanned::Spanned, token, Token}; @@ -165,24 +165,8 @@ pub struct PalletAttr { typ: PalletAttrType, } -pub struct ConfigBoundParse(syn::Ident); - -impl syn::parse::Parse for ConfigBoundParse { - fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { - let ident = input.parse::<syn::Ident>()?; - input.parse::<syn::Token![::]>()?; - input.parse::<keyword::Config>()?; - - if input.peek(syn::token::Lt) { - input.parse::<syn::AngleBracketedGenericArguments>()?; - } - - Ok(Self(ident)) - } -} - -/// Parse for `IsType<<Sef as $ident::Config>::RuntimeEvent>` and retrieve `$ident` -pub struct IsTypeBoundEventParse(syn::Ident); +/// Parse for `IsType<<Self as $path>::RuntimeEvent>` and retrieve `$path` +pub struct IsTypeBoundEventParse(syn::Path); impl syn::parse::Parse for IsTypeBoundEventParse { fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { @@ -191,15 +175,13 @@ impl syn::parse::Parse for IsTypeBoundEventParse { input.parse::<syn::Token![<]>()?; input.parse::<syn::Token![Self]>()?; input.parse::<syn::Token![as]>()?; - let ident = input.parse::<syn::Ident>()?; - input.parse::<syn::Token![::]>()?; - input.parse::<keyword::Config>()?; + let config_path = input.parse::<syn::Path>()?; input.parse::<syn::Token![>]>()?; input.parse::<syn::Token![::]>()?; input.parse::<keyword::RuntimeEvent>()?; input.parse::<syn::Token![>]>()?; - Ok(Self(ident)) + Ok(Self(config_path)) } } @@ -237,7 +219,7 @@ impl syn::parse::Parse for FromEventParse { /// Check if trait_item is `type RuntimeEvent`, if so checks its bounds are those expected. /// (Event type is reserved type) fn check_event_type( - frame_system: &syn::Ident, + frame_system: &syn::Path, trait_item: &syn::TraitItem, trait_has_instance: bool, ) -> syn::Result<bool> { @@ -249,19 +231,16 @@ fn check_event_type( no generics nor where_clause"; return Err(syn::Error::new(trait_item.span(), msg)) } - // Check bound contains IsType and From + // Check bound contains IsType and From let has_is_type_bound = type_.bounds.iter().any(|s| { syn::parse2::<IsTypeBoundEventParse>(s.to_token_stream()) - .map_or(false, |b| b.0 == *frame_system) + .map_or(false, |b| has_expected_system_config(b.0, frame_system)) }); if !has_is_type_bound { - let msg = format!( - "Invalid `type RuntimeEvent`, associated type `RuntimeEvent` is reserved and must \ - bound: `IsType<<Self as {}::Config>::RuntimeEvent>`", - frame_system, - ); + let msg = "Invalid `type RuntimeEvent`, associated type `RuntimeEvent` is reserved and must \ + bound: `IsType<<Self as frame_system::Config>::RuntimeEvent>`".to_string(); return Err(syn::Error::new(type_.span(), msg)) } @@ -295,6 +274,43 @@ fn check_event_type( } } +/// Check that the path to `frame_system::Config` is valid, this is that the path is just +/// `frame_system::Config` or when using the `frame` crate it is `frame::xyz::frame_system::Config`. +fn has_expected_system_config(path: syn::Path, frame_system: &syn::Path) -> bool { + // Check if `frame_system` is actually 'frame_system'. + if path.segments.iter().all(|s| s.ident != "frame_system") { + return false + } + + let mut expected_system_config = + match (is_using_frame_crate(&path), is_using_frame_crate(&frame_system)) { + (true, false) => + // We can't use the path to `frame_system` from `frame` if `frame_system` is not being + // in scope through `frame`. + return false, + (false, true) => + // We know that the only valid frame_system path is one that is `frame_system`, as + // `frame` re-exports it as such. + syn::parse2::<syn::Path>(quote::quote!(frame_system)).expect("is a valid path; qed"), + (_, _) => + // They are either both `frame_system` or both `frame::xyz::frame_system`. + frame_system.clone(), + }; + + expected_system_config + .segments + .push(syn::PathSegment::from(syn::Ident::new("Config", path.span()))); + + // the parse path might be something like `frame_system::Config<...>`, so we + // only compare the idents along the path. + expected_system_config + .segments + .into_iter() + .map(|ps| ps.ident) + .collect::<Vec<_>>() == + path.segments.into_iter().map(|ps| ps.ident).collect::<Vec<_>>() +} + /// Replace ident `Self` by `T` pub fn replace_self_by_t(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream { input @@ -311,7 +327,7 @@ pub fn replace_self_by_t(input: proc_macro2::TokenStream) -> proc_macro2::TokenS impl ConfigDef { pub fn try_from( - frame_system: &syn::Ident, + frame_system: &syn::Path, attr_span: proc_macro2::Span, index: usize, item: &mut syn::Item, @@ -352,8 +368,8 @@ impl ConfigDef { }; let has_frame_system_supertrait = item.supertraits.iter().any(|s| { - syn::parse2::<ConfigBoundParse>(s.to_token_stream()) - .map_or(false, |b| b.0 == *frame_system) + syn::parse2::<syn::Path>(s.to_token_stream()) + .map_or(false, |b| has_expected_system_config(b, frame_system)) }); let mut has_event_type = false; @@ -461,7 +477,8 @@ impl ConfigDef { (try `pub trait Config: frame_system::Config {{ ...` or \ `pub trait Config<I: 'static>: frame_system::Config {{ ...`). \ To disable this check, use `#[pallet::disable_frame_system_supertrait_check]`", - frame_system, found, + frame_system.to_token_stream(), + found, ); return Err(syn::Error::new(item.span(), msg)) } @@ -477,3 +494,97 @@ impl ConfigDef { }) } } + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn has_expected_system_config_works() { + let frame_system = syn::parse2::<syn::Path>(quote::quote!(frame_system)).unwrap(); + let path = syn::parse2::<syn::Path>(quote::quote!(frame_system::Config)).unwrap(); + assert!(has_expected_system_config(path, &frame_system)); + } + + #[test] + fn has_expected_system_config_works_with_assoc_type() { + let frame_system = syn::parse2::<syn::Path>(quote::quote!(frame_system)).unwrap(); + let path = + syn::parse2::<syn::Path>(quote::quote!(frame_system::Config<RuntimeCall = Call>)) + .unwrap(); + assert!(has_expected_system_config(path, &frame_system)); + } + + #[test] + fn has_expected_system_config_works_with_frame() { + let frame_system = + syn::parse2::<syn::Path>(quote::quote!(frame::deps::frame_system)).unwrap(); + let path = syn::parse2::<syn::Path>(quote::quote!(frame_system::Config)).unwrap(); + assert!(has_expected_system_config(path, &frame_system)); + } + + #[test] + fn has_expected_system_config_works_with_frame_full_path() { + let frame_system = + syn::parse2::<syn::Path>(quote::quote!(frame::deps::frame_system)).unwrap(); + let path = + syn::parse2::<syn::Path>(quote::quote!(frame::deps::frame_system::Config)).unwrap(); + assert!(has_expected_system_config(path, &frame_system)); + } + + #[test] + fn has_expected_system_config_works_with_other_frame_full_path() { + let frame_system = + syn::parse2::<syn::Path>(quote::quote!(frame::xyz::frame_system)).unwrap(); + let path = + syn::parse2::<syn::Path>(quote::quote!(frame::xyz::frame_system::Config)).unwrap(); + assert!(has_expected_system_config(path, &frame_system)); + } + + #[test] + fn has_expected_system_config_does_not_works_with_mixed_frame_full_path() { + let frame_system = + syn::parse2::<syn::Path>(quote::quote!(frame::xyz::frame_system)).unwrap(); + let path = + syn::parse2::<syn::Path>(quote::quote!(frame::deps::frame_system::Config)).unwrap(); + assert!(!has_expected_system_config(path, &frame_system)); + } + + #[test] + fn has_expected_system_config_does_not_works_with_other_mixed_frame_full_path() { + let frame_system = + syn::parse2::<syn::Path>(quote::quote!(frame::deps::frame_system)).unwrap(); + let path = + syn::parse2::<syn::Path>(quote::quote!(frame::xyz::frame_system::Config)).unwrap(); + assert!(!has_expected_system_config(path, &frame_system)); + } + + #[test] + fn has_expected_system_config_does_not_work_with_frame_full_path_if_not_frame_crate() { + let frame_system = syn::parse2::<syn::Path>(quote::quote!(frame_system)).unwrap(); + let path = + syn::parse2::<syn::Path>(quote::quote!(frame::deps::frame_system::Config)).unwrap(); + assert!(!has_expected_system_config(path, &frame_system)); + } + + #[test] + fn has_expected_system_config_unexpected_frame_system() { + let frame_system = + syn::parse2::<syn::Path>(quote::quote!(framez::deps::frame_system)).unwrap(); + let path = syn::parse2::<syn::Path>(quote::quote!(frame_system::Config)).unwrap(); + assert!(!has_expected_system_config(path, &frame_system)); + } + + #[test] + fn has_expected_system_config_unexpected_path() { + let frame_system = syn::parse2::<syn::Path>(quote::quote!(frame_system)).unwrap(); + let path = syn::parse2::<syn::Path>(quote::quote!(frame_system::ConfigSystem)).unwrap(); + assert!(!has_expected_system_config(path, &frame_system)); + } + + #[test] + fn has_expected_system_config_not_frame_system() { + let frame_system = syn::parse2::<syn::Path>(quote::quote!(something)).unwrap(); + let path = syn::parse2::<syn::Path>(quote::quote!(something::Config)).unwrap(); + assert!(!has_expected_system_config(path, &frame_system)); + } +} diff --git a/substrate/frame/support/procedural/src/pallet/parse/mod.rs b/substrate/frame/support/procedural/src/pallet/parse/mod.rs index 0f5e5f1136610..83a881751ef30 100644 --- a/substrate/frame/support/procedural/src/pallet/parse/mod.rs +++ b/substrate/frame/support/procedural/src/pallet/parse/mod.rs @@ -37,7 +37,7 @@ pub mod type_value; pub mod validate_unsigned; use composite::{keyword::CompositeKeyword, CompositeDef}; -use frame_support_procedural_tools::generate_crate_access_2018; +use frame_support_procedural_tools::generate_access_from_frame_or_crate; use syn::spanned::Spanned; /// Parsed definition of a pallet. @@ -60,15 +60,15 @@ pub struct Def { pub extra_constants: Option<extra_constants::ExtraConstantsDef>, pub composites: Vec<composite::CompositeDef>, pub type_values: Vec<type_value::TypeValueDef>, - pub frame_system: syn::Ident, - pub frame_support: syn::Ident, + pub frame_system: syn::Path, + pub frame_support: syn::Path, pub dev_mode: bool, } impl Def { pub fn try_from(mut item: syn::ItemMod, dev_mode: bool) -> syn::Result<Self> { - let frame_system = generate_crate_access_2018("frame-system")?; - let frame_support = generate_crate_access_2018("frame-support")?; + let frame_system = generate_access_from_frame_or_crate("frame-system")?; + let frame_support = generate_access_from_frame_or_crate("frame-support")?; let item_span = item.span(); let items = &mut item diff --git a/substrate/frame/support/procedural/src/pallet_error.rs b/substrate/frame/support/procedural/src/pallet_error.rs index 7fd02240a628a..693a1e9821c97 100644 --- a/substrate/frame/support/procedural/src/pallet_error.rs +++ b/substrate/frame/support/procedural/src/pallet_error.rs @@ -15,7 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use frame_support_procedural_tools::generate_crate_access_2018; +use frame_support_procedural_tools::generate_access_from_frame_or_crate; use quote::ToTokens; // Derive `PalletError` @@ -25,7 +25,7 @@ pub fn derive_pallet_error(input: proc_macro::TokenStream) -> proc_macro::TokenS Err(e) => return e.to_compile_error().into(), }; - let frame_support = match generate_crate_access_2018("frame-support") { + let frame_support = match generate_access_from_frame_or_crate("frame-support") { Ok(c) => c, Err(e) => return e.into_compile_error().into(), }; @@ -111,7 +111,7 @@ pub fn derive_pallet_error(input: proc_macro::TokenStream) -> proc_macro::TokenS fn generate_field_types( field: &syn::Field, - scrate: &syn::Ident, + scrate: &syn::Path, ) -> syn::Result<Option<proc_macro2::TokenStream>> { let attrs = &field.attrs; @@ -143,7 +143,7 @@ fn generate_field_types( fn generate_variant_field_types( variant: &syn::Variant, - scrate: &syn::Ident, + scrate: &syn::Path, ) -> syn::Result<Option<Vec<proc_macro2::TokenStream>>> { let attrs = &variant.attrs; diff --git a/substrate/frame/support/procedural/src/storage_alias.rs b/substrate/frame/support/procedural/src/storage_alias.rs index 4903fd1c129c7..c0b4089a2748f 100644 --- a/substrate/frame/support/procedural/src/storage_alias.rs +++ b/substrate/frame/support/procedural/src/storage_alias.rs @@ -18,7 +18,7 @@ //! Implementation of the `storage_alias` attribute macro. use crate::{counter_prefix, pallet::parse::helper}; -use frame_support_procedural_tools::generate_crate_access_2018; +use frame_support_procedural_tools::generate_access_from_frame_or_crate; use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; use syn::{ @@ -199,7 +199,7 @@ impl StorageType { /// Generate the actual type declaration. fn generate_type_declaration( &self, - crate_: &Ident, + crate_: &syn::Path, storage_instance: &StorageInstance, storage_name: &Ident, storage_generics: Option<&SimpleGenerics>, @@ -475,7 +475,7 @@ enum PrefixType { /// Implementation of the `storage_alias` attribute macro. pub fn storage_alias(attributes: TokenStream, input: TokenStream) -> Result<TokenStream> { let input = syn::parse2::<Input>(input)?; - let crate_ = generate_crate_access_2018("frame-support")?; + let crate_ = generate_access_from_frame_or_crate("frame-support")?; let prefix_type = if attributes.is_empty() { PrefixType::Compatibility @@ -527,7 +527,7 @@ struct StorageInstance { /// Generate the [`StorageInstance`] for the storage alias. fn generate_storage_instance( - crate_: &Ident, + crate_: &syn::Path, storage_name: &Ident, storage_generics: Option<&SimpleGenerics>, storage_where_clause: Option<&WhereClause>, diff --git a/substrate/frame/support/procedural/src/transactional.rs b/substrate/frame/support/procedural/src/transactional.rs index 23117ffa39c4e..e9d4f84b797a9 100644 --- a/substrate/frame/support/procedural/src/transactional.rs +++ b/substrate/frame/support/procedural/src/transactional.rs @@ -15,7 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use frame_support_procedural_tools::generate_crate_access_2018; +use frame_support_procedural_tools::generate_access_from_frame_or_crate; use proc_macro::TokenStream; use quote::quote; use syn::{ItemFn, Result}; @@ -23,7 +23,7 @@ use syn::{ItemFn, Result}; pub fn transactional(_attr: TokenStream, input: TokenStream) -> Result<TokenStream> { let ItemFn { attrs, vis, sig, block } = syn::parse(input)?; - let crate_ = generate_crate_access_2018("frame-support")?; + let crate_ = generate_access_from_frame_or_crate("frame-support")?; let output = quote! { #(#attrs)* #vis #sig { @@ -45,7 +45,7 @@ pub fn transactional(_attr: TokenStream, input: TokenStream) -> Result<TokenStre pub fn require_transactional(_attr: TokenStream, input: TokenStream) -> Result<TokenStream> { let ItemFn { attrs, vis, sig, block } = syn::parse(input)?; - let crate_ = generate_crate_access_2018("frame-support")?; + let crate_ = generate_access_from_frame_or_crate("frame-support")?; let output = quote! { #(#attrs)* #vis #sig { diff --git a/substrate/frame/support/procedural/src/tt_macro.rs b/substrate/frame/support/procedural/src/tt_macro.rs index 01611f5dc4a4d..d3712742189f8 100644 --- a/substrate/frame/support/procedural/src/tt_macro.rs +++ b/substrate/frame/support/procedural/src/tt_macro.rs @@ -18,7 +18,6 @@ //! Implementation of the `create_tt_return_macro` macro use crate::COUNTER; -use frame_support_procedural_tools::generate_crate_access_2018; use proc_macro2::{Ident, TokenStream}; use quote::format_ident; @@ -65,9 +64,9 @@ impl syn::parse::Parse for CreateTtReturnMacroDef { /// macro_rules! my_tt_macro { /// { /// $caller:tt -/// $(frame_support = [{ $($frame_support:ident)::* }])? +/// $(your_tt_return = [{ $my_tt_return:path }])? /// } => { -/// frame_support::__private::tt_return! { +/// $my_tt_return! { /// $caller /// foo = [{ bar }] /// } @@ -78,10 +77,6 @@ pub fn create_tt_return_macro(input: proc_macro::TokenStream) -> proc_macro::Tok let CreateTtReturnMacroDef { name, args } = syn::parse_macro_input!(input as CreateTtReturnMacroDef); - let frame_support = match generate_crate_access_2018("frame-support") { - Ok(i) => i, - Err(e) => return e.into_compile_error().into(), - }; let (keys, values): (Vec<_>, Vec<_>) = args.into_iter().unzip(); let count = COUNTER.with(|counter| counter.borrow_mut().inc()); let unique_name = format_ident!("{}_{}", name, count); @@ -92,9 +87,9 @@ pub fn create_tt_return_macro(input: proc_macro::TokenStream) -> proc_macro::Tok macro_rules! #unique_name { { $caller:tt - $(frame_support = [{ $($frame_support:ident)::* }])? + $(your_tt_return = [{ $my_tt_macro:path }])? } => { - #frame_support::__private::tt_return! { + $my_tt_return! { $caller #( #keys = [{ #values }] diff --git a/substrate/frame/support/procedural/tools/src/lib.rs b/substrate/frame/support/procedural/tools/src/lib.rs index 541accc8ab96a..62c77ef7a99ae 100644 --- a/substrate/frame/support/procedural/tools/src/lib.rs +++ b/substrate/frame/support/procedural/tools/src/lib.rs @@ -39,24 +39,44 @@ fn generate_hidden_includes_mod_name(unique_id: &str) -> Ident { /// Generates the access to the `frame-support` crate. pub fn generate_crate_access(unique_id: &str, def_crate: &str) -> TokenStream { if std::env::var("CARGO_PKG_NAME").unwrap() == def_crate { - quote::quote!(frame_support) + let frame_support = match generate_access_from_frame_or_crate("frame-support") { + Ok(c) => c, + Err(e) => return e.into_compile_error().into(), + }; + quote::quote!(#frame_support) } else { let mod_name = generate_hidden_includes_mod_name(unique_id); quote::quote!( self::#mod_name::hidden_include ) } } +/// Check if a path is using the `frame` crate or not. +/// +/// This will usually check the output of [`generate_access_from_frame_or_crate`]. +/// We want to know if whatever the `path` takes us to, is exported from `frame` or not. In that +/// case `path` would start with `frame`, something like `frame::x::y:z`. +pub fn is_using_frame_crate(path: &syn::Path) -> bool { + path.segments.first().map(|s| s.ident == "frame").unwrap_or(false) +} + /// Generate the crate access for the crate using 2018 syntax. /// -/// for `frame-support` output will for example be `frame_support`. -pub fn generate_crate_access_2018(def_crate: &str) -> Result<syn::Ident, Error> { - match crate_name(def_crate) { - Ok(FoundCrate::Itself) => { - let name = def_crate.to_string().replace("-", "_"); - Ok(syn::Ident::new(&name, Span::call_site())) - }, - Ok(FoundCrate::Name(name)) => Ok(Ident::new(&name, Span::call_site())), - Err(e) => Err(Error::new(Span::call_site(), e)), +/// If `frame` is in scope, it will use `frame::deps::<def_crate>`. Else, it will try and find +/// `<def_crate>` directly. +pub fn generate_access_from_frame_or_crate(def_crate: &str) -> Result<syn::Path, Error> { + if let Some(path) = get_frame_crate_path(def_crate) { + Ok(path) + } else { + let ident = match crate_name(def_crate) { + Ok(FoundCrate::Itself) => { + let name = def_crate.to_string().replace("-", "_"); + Ok(syn::Ident::new(&name, Span::call_site())) + }, + Ok(FoundCrate::Name(name)) => Ok(Ident::new(&name, Span::call_site())), + Err(e) => Err(Error::new(Span::call_site(), e)), + }?; + + Ok(syn::Path::from(ident)) } } @@ -64,21 +84,41 @@ pub fn generate_crate_access_2018(def_crate: &str) -> Result<syn::Ident, Error> pub fn generate_hidden_includes(unique_id: &str, def_crate: &str) -> TokenStream { let mod_name = generate_hidden_includes_mod_name(unique_id); - match crate_name(def_crate) { - Ok(FoundCrate::Itself) => quote!(), - Ok(FoundCrate::Name(name)) => { - let name = Ident::new(&name, Span::call_site()); - quote::quote!( - #[doc(hidden)] - mod #mod_name { - pub extern crate #name as hidden_include; - } - ) - }, - Err(e) => { - let err = Error::new(Span::call_site(), e).to_compile_error(); - quote!( #err ) - }, + if let Some(path) = get_frame_crate_path(def_crate) { + quote::quote!( + #[doc(hidden)] + mod #mod_name { + pub use #path as hidden_include; + } + ) + } else { + match crate_name(def_crate) { + Ok(FoundCrate::Itself) => quote!(), + Ok(FoundCrate::Name(name)) => { + let name = Ident::new(&name, Span::call_site()); + quote::quote!( + #[doc(hidden)] + mod #mod_name { + pub extern crate #name as hidden_include; + } + ) + }, + Err(e) => { + let err = Error::new(Span::call_site(), e).to_compile_error(); + quote!( #err ) + }, + } + } +} + +/// Generates the path to the frame crate deps. +fn get_frame_crate_path(def_crate: &str) -> Option<syn::Path> { + // This does not work if the frame crate is renamed. + if let Ok(FoundCrate::Name(name)) = crate_name(&"frame") { + let path = format!("{}::deps::{}", name, def_crate.to_string().replace("-", "_")); + Some(syn::parse_str::<syn::Path>(&path).expect("is a valid path; qed")) + } else { + None } } diff --git a/substrate/frame/support/test/compile_pass/Cargo.toml b/substrate/frame/support/test/compile_pass/Cargo.toml index 9c165cd03e866..167aec8a171c7 100644 --- a/substrate/frame/support/test/compile_pass/Cargo.toml +++ b/substrate/frame/support/test/compile_pass/Cargo.toml @@ -15,7 +15,7 @@ targets = ["x86_64-unknown-linux-gnu"] codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] } scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } renamed-frame-support = { package = "frame-support", path = "../..", default-features = false} -frame-system = { path = "../../../system", default-features = false} +renamed-frame-system = { package = "frame-system", path = "../../../system", default-features = false} sp-core = { path = "../../../../primitives/core", default-features = false} sp-runtime = { path = "../../../../primitives/runtime", default-features = false} sp-version = { path = "../../../../primitives/version", default-features = false} @@ -24,8 +24,8 @@ sp-version = { path = "../../../../primitives/version", default-features = false default = [ "std" ] std = [ "codec/std", - "frame-system/std", "renamed-frame-support/std", + "renamed-frame-system/std", "scale-info/std", "sp-core/std", "sp-runtime/std", diff --git a/substrate/frame/support/test/compile_pass/src/lib.rs b/substrate/frame/support/test/compile_pass/src/lib.rs index bf90d73acb320..6ea37fb27e72f 100644 --- a/substrate/frame/support/test/compile_pass/src/lib.rs +++ b/substrate/frame/support/test/compile_pass/src/lib.rs @@ -16,7 +16,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -//! Test that `construct_runtime!` also works when `frame-support` is renamed in the `Cargo.toml`. +//! Test that `construct_runtime!` also works when `frame-support` or `frame-system` are renamed in +//! the `Cargo.toml`. #![cfg_attr(not(feature = "std"), no_std)] @@ -50,7 +51,7 @@ parameter_types! { pub const Version: RuntimeVersion = VERSION; } -impl frame_system::Config for Runtime { +impl renamed_frame_system::Config for Runtime { type BaseCallFilter = Everything; type BlockWeights = (); type BlockLength = (); @@ -82,6 +83,6 @@ pub type UncheckedExtrinsic = generic::UncheckedExtrinsic<u32, RuntimeCall, Sign construct_runtime!( pub struct Runtime { - System: frame_system, + System: renamed_frame_system, } ); diff --git a/substrate/frame/support/test/stg_frame_crate/Cargo.toml b/substrate/frame/support/test/stg_frame_crate/Cargo.toml new file mode 100644 index 0000000000000..340f08905c32e --- /dev/null +++ b/substrate/frame/support/test/stg_frame_crate/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "frame-support-test-stg-frame-crate" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +publish = false +homepage = "https://substrate.io" +repository.workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] } +frame = { path = "frame", default-features = false} +scale-info = { version = "2.0.0", default-features = false, features = ["derive"] } + +[features] +default = [ "std" ] +std = [ "codec/std", "frame/std", "scale-info/std" ] diff --git a/substrate/frame/support/test/stg_frame_crate/frame/Cargo.toml b/substrate/frame/support/test/stg_frame_crate/frame/Cargo.toml new file mode 100644 index 0000000000000..d99ac2d2d46ee --- /dev/null +++ b/substrate/frame/support/test/stg_frame_crate/frame/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "frame" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +publish = false +homepage = "https://substrate.io" +repository.workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +frame-system = { path = "../../../../system", default-features = false} +frame-support = { path = "../../..", default-features = false} + +[features] +default = [ "std" ] +std = [ "frame-support/std", "frame-system/std" ] diff --git a/substrate/frame/support/test/stg_frame_crate/frame/src/lib.rs b/substrate/frame/support/test/stg_frame_crate/frame/src/lib.rs new file mode 100644 index 0000000000000..dba99f1bbe73f --- /dev/null +++ b/substrate/frame/support/test/stg_frame_crate/frame/src/lib.rs @@ -0,0 +1,21 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// 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. + +pub mod deps { + pub use frame_support; + pub use frame_system; +} diff --git a/substrate/frame/support/test/stg_frame_crate/src/lib.rs b/substrate/frame/support/test/stg_frame_crate/src/lib.rs new file mode 100644 index 0000000000000..501fcf6a70d7a --- /dev/null +++ b/substrate/frame/support/test/stg_frame_crate/src/lib.rs @@ -0,0 +1,75 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// 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. + +// ! A basic pallet to test it compiles along with a runtime using it when `frame_system` and +// `frame_support` are reexported by a `frame` crate. + +use frame::deps::{frame_support, frame_system}; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet<T>(_); + + #[pallet::config] + // The only valid syntax here is the following or + // ``` + // pub trait Config: frame::deps::frame_system::Config {} + // ``` + pub trait Config: frame_system::Config {} + + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig<T: Config> { + #[serde(skip)] + _config: core::marker::PhantomData<T>, + } + + #[pallet::genesis_build] + impl<T: Config> BuildGenesisConfig for GenesisConfig<T> { + fn build(&self) {} + } +} + +#[cfg(test)] +// Dummy test to make sure a runtime would compile. +mod tests { + use super::{ + frame_support::{construct_runtime, derive_impl}, + frame_system, pallet, + }; + + type Block = frame_system::mocking::MockBlock<Runtime>; + + impl crate::pallet::Config for Runtime {} + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] + impl frame_system::Config for Runtime { + type Block = Block; + } + + construct_runtime! { + pub struct Runtime + { + System: frame_system::{Pallet, Call, Storage, Config<T>, Event<T>}, + Pallet: pallet::{Pallet, Config<T>}, + } + } +} diff --git a/substrate/frame/support/test/tests/pallet_ui/event_type_invalid_bound.rs b/substrate/frame/support/test/tests/pallet_ui/event_type_invalid_bound.rs index 665d2a984cf04..34b563ffb6433 100644 --- a/substrate/frame/support/test/tests/pallet_ui/event_type_invalid_bound.rs +++ b/substrate/frame/support/test/tests/pallet_ui/event_type_invalid_bound.rs @@ -41,5 +41,4 @@ mod pallet { } } -fn main() { -} +fn main() {} diff --git a/substrate/frame/support/test/tests/pallet_ui/pass/event_type_bound_system_config_assoc_type.rs b/substrate/frame/support/test/tests/pallet_ui/pass/event_type_bound_system_config_assoc_type.rs new file mode 100644 index 0000000000000..0a70c4a5e68a8 --- /dev/null +++ b/substrate/frame/support/test/tests/pallet_ui/pass/event_type_bound_system_config_assoc_type.rs @@ -0,0 +1,44 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// 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. + +#[frame_support::pallet] +mod pallet { + use frame_support::pallet_prelude::{Hooks, IsType}; + use frame_system::pallet_prelude::BlockNumberFor; + + #[pallet::config] + pub trait Config: frame_system::Config<Hash = sp_core::H256> { + type Bar: Clone + std::fmt::Debug + Eq; + type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>; + } + + #[pallet::pallet] + pub struct Pallet<T>(core::marker::PhantomData<T>); + + #[pallet::hooks] + impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {} + + #[pallet::call] + impl<T: Config> Pallet<T> {} + + #[pallet::event] + pub enum Event<T: Config> { + B { b: T::Bar }, + } +} + +fn main() {} From 9f7656df15690965215bf24171c45640f4f1a73b Mon Sep 17 00:00:00 2001 From: Julian Eager <eagr@tutanota.com> Date: Sun, 15 Oct 2023 05:06:00 +0800 Subject: [PATCH 45/54] Discard `Executor` (#1855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #622 Pros: * simpler interface, just functions: `create_runtime_from_artifact_bytes()` and `execute_artifact()` Cons: * extra overhead of constructing executor semantics each time I could make it a combination of * `create_runtime_config(params)` (such that we could clone the constructed semantics) * `create_runtime(blob, config)` * `execute_artifact(blob, config, params)` Not sure if it's worth it though. --------- Co-authored-by: Bastian Köcher <git@kchr.de> --- .../node/core/pvf/common/src/executor_intf.rs | 120 ++++++++---------- .../node/core/pvf/execute-worker/src/lib.rs | 18 +-- .../node/core/pvf/prepare-worker/src/lib.rs | 16 +-- polkadot/node/core/pvf/src/testing.rs | 8 +- .../procedural/src/pallet/expand/warnings.rs | 2 + 5 files changed, 77 insertions(+), 87 deletions(-) diff --git a/polkadot/node/core/pvf/common/src/executor_intf.rs b/polkadot/node/core/pvf/common/src/executor_intf.rs index 508a12998fc9d..58d2a90371cf4 100644 --- a/polkadot/node/core/pvf/common/src/executor_intf.rs +++ b/polkadot/node/core/pvf/common/src/executor_intf.rs @@ -95,6 +95,60 @@ pub const DEFAULT_CONFIG: Config = Config { }, }; +/// Executes the given PVF in the form of a compiled artifact and returns the result of +/// execution upon success. +/// +/// # Safety +/// +/// The caller must ensure that the compiled artifact passed here was: +/// 1) produced by `prepare`, +/// 2) was not modified, +/// +/// Failure to adhere to these requirements might lead to crashes and arbitrary code execution. +pub unsafe fn execute_artifact( + compiled_artifact_blob: &[u8], + executor_params: &ExecutorParams, + params: &[u8], +) -> Result<Vec<u8>, String> { + let mut extensions = sp_externalities::Extensions::new(); + + extensions.register(sp_core::traits::ReadRuntimeVersionExt::new(ReadRuntimeVersion)); + + let mut ext = ValidationExternalities(extensions); + + match sc_executor::with_externalities_safe(&mut ext, || { + let runtime = create_runtime_from_artifact_bytes(compiled_artifact_blob, executor_params)?; + runtime.new_instance()?.call(InvokeMethod::Export("validate_block"), params) + }) { + Ok(Ok(ok)) => Ok(ok), + Ok(Err(err)) | Err(err) => Err(err), + } + .map_err(|err| format!("execute error: {:?}", err)) +} + +/// Constructs the runtime for the given PVF, given the artifact bytes. +/// +/// # Safety +/// +/// The caller must ensure that the compiled artifact passed here was: +/// 1) produced by `prepare`, +/// 2) was not modified, +/// +/// Failure to adhere to these requirements might lead to crashes and arbitrary code execution. +pub unsafe fn create_runtime_from_artifact_bytes( + compiled_artifact_blob: &[u8], + executor_params: &ExecutorParams, +) -> Result<WasmtimeRuntime, WasmError> { + let mut config = DEFAULT_CONFIG.clone(); + config.semantics = + params_to_wasmtime_semantics(executor_params).map_err(|err| WasmError::Other(err))?; + + sc_executor_wasmtime::create_runtime_from_artifact_bytes::<HostFunctions>( + compiled_artifact_blob, + config, + ) +} + pub fn params_to_wasmtime_semantics(par: &ExecutorParams) -> Result<Semantics, String> { let mut sem = DEFAULT_CONFIG.semantics.clone(); let mut stack_limit = if let Some(stack_limit) = sem.deterministic_stack_limit.clone() { @@ -121,72 +175,6 @@ pub fn params_to_wasmtime_semantics(par: &ExecutorParams) -> Result<Semantics, S Ok(sem) } -/// A WASM executor with a given configuration. It is instantiated once per execute worker and is -/// specific to that worker. -#[derive(Clone)] -pub struct Executor { - config: Config, -} - -impl Executor { - pub fn new(params: ExecutorParams) -> Result<Self, String> { - let mut config = DEFAULT_CONFIG.clone(); - config.semantics = params_to_wasmtime_semantics(¶ms)?; - - Ok(Self { config }) - } - - /// Executes the given PVF in the form of a compiled artifact and returns the result of - /// execution upon success. - /// - /// # Safety - /// - /// The caller must ensure that the compiled artifact passed here was: - /// 1) produced by `prepare`, - /// 2) was not modified, - /// - /// Failure to adhere to these requirements might lead to crashes and arbitrary code execution. - pub unsafe fn execute( - &self, - compiled_artifact_blob: &[u8], - params: &[u8], - ) -> Result<Vec<u8>, String> { - let mut extensions = sp_externalities::Extensions::new(); - - extensions.register(sp_core::traits::ReadRuntimeVersionExt::new(ReadRuntimeVersion)); - - let mut ext = ValidationExternalities(extensions); - - match sc_executor::with_externalities_safe(&mut ext, || { - let runtime = self.create_runtime_from_bytes(compiled_artifact_blob)?; - runtime.new_instance()?.call(InvokeMethod::Export("validate_block"), params) - }) { - Ok(Ok(ok)) => Ok(ok), - Ok(Err(err)) | Err(err) => Err(err), - } - .map_err(|err| format!("execute error: {:?}", err)) - } - - /// Constructs the runtime for the given PVF, given the artifact bytes. - /// - /// # Safety - /// - /// The caller must ensure that the compiled artifact passed here was: - /// 1) produced by `prepare`, - /// 2) was not modified, - /// - /// Failure to adhere to these requirements might lead to crashes and arbitrary code execution. - pub unsafe fn create_runtime_from_bytes( - &self, - compiled_artifact_blob: &[u8], - ) -> Result<WasmtimeRuntime, WasmError> { - sc_executor_wasmtime::create_runtime_from_artifact_bytes::<HostFunctions>( - compiled_artifact_blob, - self.config.clone(), - ) - } -} - /// Available host functions. We leave out: /// /// 1. storage related stuff (PVF doesn't have a notion of a persistent storage/trie) diff --git a/polkadot/node/core/pvf/execute-worker/src/lib.rs b/polkadot/node/core/pvf/execute-worker/src/lib.rs index 65185b6f45f36..af73eb16e685a 100644 --- a/polkadot/node/core/pvf/execute-worker/src/lib.rs +++ b/polkadot/node/core/pvf/execute-worker/src/lib.rs @@ -16,7 +16,9 @@ //! Contains the logic for executing PVFs. Used by the polkadot-execute-worker binary. -pub use polkadot_node_core_pvf_common::{executor_intf::Executor, worker_dir, SecurityStatus}; +pub use polkadot_node_core_pvf_common::{ + executor_intf::execute_artifact, worker_dir, SecurityStatus, +}; // NOTE: Initializing logging in e.g. tests will not have an effect in the workers, as they are // separate spawned processes. Run with e.g. `RUST_LOG=parachain::pvf-execute-worker=trace`. @@ -35,7 +37,7 @@ use polkadot_node_core_pvf_common::{ }, }; use polkadot_parachain_primitives::primitives::ValidationResult; -use polkadot_primitives::executor_params::DEFAULT_NATIVE_STACK_MAX; +use polkadot_primitives::{executor_params::DEFAULT_NATIVE_STACK_MAX, ExecutorParams}; use std::{ os::unix::net::UnixStream, path::PathBuf, @@ -141,9 +143,6 @@ pub fn worker_entrypoint( let artifact_path = worker_dir::execute_artifact(&worker_dir_path); let Handshake { executor_params } = recv_handshake(&mut stream)?; - let executor = Executor::new(executor_params).map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("cannot create executor: {}", e)) - })?; loop { let (params, execution_timeout) = recv_request(&mut stream)?; @@ -185,14 +184,15 @@ pub fn worker_entrypoint( Arc::clone(&condvar), WaitOutcome::TimedOut, )?; - let executor_2 = executor.clone(); + + let executor_params_2 = executor_params.clone(); let execute_thread = thread::spawn_worker_thread_with_stack_size( "execute thread", move || { validate_using_artifact( &compiled_artifact_blob, + &executor_params_2, ¶ms, - executor_2, cpu_time_start, ) }, @@ -257,15 +257,15 @@ pub fn worker_entrypoint( fn validate_using_artifact( compiled_artifact_blob: &[u8], + executor_params: &ExecutorParams, params: &[u8], - executor: Executor, cpu_time_start: ProcessTime, ) -> Response { let descriptor_bytes = match unsafe { // SAFETY: this should be safe since the compiled artifact passed here comes from the // file created by the prepare workers. These files are obtained by calling // [`executor_intf::prepare`]. - executor.execute(compiled_artifact_blob, params) + execute_artifact(compiled_artifact_blob, executor_params, params) } { Err(err) => return Response::format_invalid("execute", &err), Ok(d) => d, diff --git a/polkadot/node/core/pvf/prepare-worker/src/lib.rs b/polkadot/node/core/pvf/prepare-worker/src/lib.rs index fcc7f6754a7e2..fa5d3656a35e6 100644 --- a/polkadot/node/core/pvf/prepare-worker/src/lib.rs +++ b/polkadot/node/core/pvf/prepare-worker/src/lib.rs @@ -32,7 +32,7 @@ use crate::memory_stats::memory_tracker::{get_memory_tracker_loop_stats, memory_ use parity_scale_codec::{Decode, Encode}; use polkadot_node_core_pvf_common::{ error::{PrepareError, PrepareResult}, - executor_intf::Executor, + executor_intf::create_runtime_from_artifact_bytes, framed_recv_blocking, framed_send_blocking, prepare::{MemoryStats, PrepareJobKind, PrepareStats}, pvf::PvfPrepData, @@ -145,7 +145,7 @@ pub fn worker_entrypoint( let preparation_timeout = pvf.prep_timeout(); let prepare_job_kind = pvf.prep_kind(); - let executor_params = (*pvf.executor_params()).clone(); + let executor_params = pvf.executor_params(); // Conditional variable to notify us when a thread is done. let condvar = thread::get_condvar(); @@ -191,7 +191,10 @@ pub fn worker_entrypoint( // anyway. if let PrepareJobKind::Prechecking = prepare_job_kind { result = result.and_then(|output| { - runtime_construction_check(output.0.as_ref(), executor_params)?; + runtime_construction_check( + output.0.as_ref(), + executor_params.as_ref(), + )?; Ok(output) }); } @@ -317,13 +320,10 @@ fn prepare_artifact( /// Try constructing the runtime to catch any instantiation errors during pre-checking. fn runtime_construction_check( artifact_bytes: &[u8], - executor_params: ExecutorParams, + executor_params: &ExecutorParams, ) -> Result<(), PrepareError> { - let executor = Executor::new(executor_params) - .map_err(|e| PrepareError::RuntimeConstruction(format!("cannot create executor: {}", e)))?; - // SAFETY: We just compiled this artifact. - let result = unsafe { executor.create_runtime_from_bytes(&artifact_bytes) }; + let result = unsafe { create_runtime_from_artifact_bytes(artifact_bytes, executor_params) }; result .map(|_runtime| ()) .map_err(|err| PrepareError::RuntimeConstruction(format!("{:?}", err))) diff --git a/polkadot/node/core/pvf/src/testing.rs b/polkadot/node/core/pvf/src/testing.rs index 4301afc3cc7ea..ca69ef9e4d047 100644 --- a/polkadot/node/core/pvf/src/testing.rs +++ b/polkadot/node/core/pvf/src/testing.rs @@ -29,20 +29,20 @@ pub fn validate_candidate( code: &[u8], params: &[u8], ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { - use polkadot_node_core_pvf_execute_worker::Executor; + use polkadot_node_core_pvf_execute_worker::execute_artifact; use polkadot_node_core_pvf_prepare_worker::{prepare, prevalidate}; let code = sp_maybe_compressed_blob::decompress(code, 10 * 1024 * 1024) .expect("Decompressing code failed"); let blob = prevalidate(&code)?; - let compiled_artifact_blob = prepare(blob, &ExecutorParams::default())?; + let executor_params = ExecutorParams::default(); + let compiled_artifact_blob = prepare(blob, &executor_params)?; - let executor = Executor::new(ExecutorParams::default())?; let result = unsafe { // SAFETY: This is trivially safe since the artifact is obtained by calling `prepare` // and is written into a temporary directory in an unmodified state. - executor.execute(&compiled_artifact_blob, params)? + execute_artifact(&compiled_artifact_blob, &executor_params, params)? }; Ok(result) diff --git a/substrate/frame/support/procedural/src/pallet/expand/warnings.rs b/substrate/frame/support/procedural/src/pallet/expand/warnings.rs index 030e3ddaf3232..110c4fa51987a 100644 --- a/substrate/frame/support/procedural/src/pallet/expand/warnings.rs +++ b/substrate/frame/support/procedural/src/pallet/expand/warnings.rs @@ -33,6 +33,7 @@ pub(crate) fn weight_witness_warning( if dev_mode { return } + let CallWeightDef::Immediate(w) = &method.weight else { return }; let partial_warning = Warning::new_deprecated("UncheckedWeightWitness") @@ -64,6 +65,7 @@ pub(crate) fn weight_constant_warning( if dev_mode { return } + let syn::Expr::Lit(lit) = weight else { return }; let warning = Warning::new_deprecated("ConstantWeight") From c9b51cd49c4315f2c744ab977941136e6dd6a571 Mon Sep 17 00:00:00 2001 From: S E R A Y A <takahser@users.noreply.github.com> Date: Sun, 15 Oct 2023 10:00:58 +0200 Subject: [PATCH 46/54] add link to rfc-0001 in broker README (#1862) # Description - What does this PR do? - link added - Why are these changes needed? - improve docs - How were these changes implemented and what do they affect? - only concerns docs --- docs/DOCUMENTATION_GUIDELINES.md | 2 +- substrate/frame/broker/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/DOCUMENTATION_GUIDELINES.md b/docs/DOCUMENTATION_GUIDELINES.md index 29029b894c761..5d1164e8ca89f 100644 --- a/docs/DOCUMENTATION_GUIDELINES.md +++ b/docs/DOCUMENTATION_GUIDELINES.md @@ -210,7 +210,7 @@ The guidelines so far have been general in nature, and are applicable to crates pallets. The following is relevant to how to document parts of a crate that is a pallet. See -[`pallet-fast-unstake`](../frame/fast-unstake/src/lib.rs) as one example of adhering these guidelines. +[`pallet-fast-unstake`](../substrate/frame/fast-unstake/src/lib.rs) as one example of adhering these guidelines. --- diff --git a/substrate/frame/broker/README.md b/substrate/frame/broker/README.md index a3c4b251a4527..eae0442dbf69b 100644 --- a/substrate/frame/broker/README.md +++ b/substrate/frame/broker/README.md @@ -2,7 +2,7 @@ Brokerage tool for managing Polkadot Core scheduling. -Properly described in RFC-0001 Agile Coretime. +Properly described in [RFC-0001 Agile Coretime](https://github.com/polkadot-fellows/RFCs/blob/main/text/0001-agile-coretime.md). ## Implementation Specifics From 9e1447042b648149d515d53ccdef3bd3e4e37ef6 Mon Sep 17 00:00:00 2001 From: Julian Eager <eagr@tutanota.com> Date: Sun, 15 Oct 2023 16:39:03 +0800 Subject: [PATCH 47/54] Include polkadot version in artifact path (#1828) closes #695 Could potentially be helpful to preserving caches when applicable, as discussed in #685 kusama address: FvpsvV1GQAAbwqX6oyRjemgdKV11QU5bXsMg9xsonD1FLGK --- Cargo.lock | 1 + polkadot/cli/Cargo.toml | 1 + polkadot/cli/src/cli.rs | 12 +----- polkadot/node/core/pvf/src/artifacts.rs | 55 ++++++++++++++++++------- polkadot/node/primitives/src/lib.rs | 7 ++++ 5 files changed, 50 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed5e0a29f71c1..855700f3c201a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11635,6 +11635,7 @@ dependencies = [ "futures", "log", "polkadot-node-metrics", + "polkadot-node-primitives", "polkadot-service", "pyroscope", "pyroscope_pprofrs", diff --git a/polkadot/cli/Cargo.toml b/polkadot/cli/Cargo.toml index 8057342aaea0c..9cad7e88dfe92 100644 --- a/polkadot/cli/Cargo.toml +++ b/polkadot/cli/Cargo.toml @@ -33,6 +33,7 @@ try-runtime-cli = { path = "../../substrate/utils/frame/try-runtime/cli", option sc-cli = { path = "../../substrate/client/cli", optional = true } sc-service = { path = "../../substrate/client/service", optional = true } polkadot-node-metrics = { path = "../node/metrics" } +polkadot-node-primitives = { path = "../node/primitives" } sc-tracing = { path = "../../substrate/client/tracing", optional = true } sc-sysinfo = { path = "../../substrate/client/sysinfo" } sc-executor = { path = "../../substrate/client/executor" } diff --git a/polkadot/cli/src/cli.rs b/polkadot/cli/src/cli.rs index faca2b25e23a3..bc060c21fba70 100644 --- a/polkadot/cli/src/cli.rs +++ b/polkadot/cli/src/cli.rs @@ -16,19 +16,11 @@ //! Polkadot CLI library. +pub use polkadot_node_primitives::NODE_VERSION; + use clap::Parser; use std::path::PathBuf; -/// The version of the node. -/// -/// This is the version that is used for versioning this node binary. -/// By default the `minor` version is bumped in every release. `Major` or `patch` releases are only -/// expected in very rare cases. -/// -/// The worker binaries associated to the node binary should ensure that they are using the same -/// version as the main node that started them. -pub const NODE_VERSION: &'static str = "1.1.0"; - #[allow(missing_docs)] #[derive(Debug, Parser)] pub enum Subcommand { diff --git a/polkadot/node/core/pvf/src/artifacts.rs b/polkadot/node/core/pvf/src/artifacts.rs index 5a1767af75b77..d7b15ece7b294 100644 --- a/polkadot/node/core/pvf/src/artifacts.rs +++ b/polkadot/node/core/pvf/src/artifacts.rs @@ -58,6 +58,7 @@ use crate::host::PrepareResultSender; use always_assert::always; use polkadot_node_core_pvf_common::{error::PrepareError, prepare::PrepareStats, pvf::PvfPrepData}; +use polkadot_node_primitives::NODE_VERSION; use polkadot_parachain_primitives::primitives::ValidationCodeHash; use polkadot_primitives::ExecutorParamsHash; use std::{ @@ -75,6 +76,7 @@ pub struct ArtifactId { impl ArtifactId { const PREFIX: &'static str = "wasmtime_"; + const NODE_VERSION_PREFIX: &'static str = "polkadot_v"; /// Creates a new artifact ID with the given hash. pub fn new(code_hash: ValidationCodeHash, executor_params_hash: ExecutorParamsHash) -> Self { @@ -92,8 +94,13 @@ impl ArtifactId { use polkadot_core_primitives::Hash; use std::str::FromStr as _; - let file_name = file_name.strip_prefix(Self::PREFIX)?; - let (code_hash_str, executor_params_hash_str) = file_name.split_once('_')?; + let file_name = + file_name.strip_prefix(Self::PREFIX)?.strip_prefix(Self::NODE_VERSION_PREFIX)?; + + // [ node version | code hash | param hash ] + let parts: Vec<&str> = file_name.split('_').collect(); + let (_node_ver, code_hash_str, executor_params_hash_str) = (parts[0], parts[1], parts[2]); + let code_hash = Hash::from_str(code_hash_str).ok()?.into(); let executor_params_hash = ExecutorParamsHash::from_hash(Hash::from_str(executor_params_hash_str).ok()?); @@ -103,8 +110,14 @@ impl ArtifactId { /// Returns the expected path to this artifact given the root of the cache. pub fn path(&self, cache_path: &Path) -> PathBuf { - let file_name = - format!("{}{:#x}_{:#x}", Self::PREFIX, self.code_hash, self.executor_params_hash); + let file_name = format!( + "{}{}{}_{:#x}_{:#x}", + Self::PREFIX, + Self::NODE_VERSION_PREFIX, + NODE_VERSION, + self.code_hash, + self.executor_params_hash + ); cache_path.join(file_name) } } @@ -253,20 +266,27 @@ impl Artifacts { #[cfg(test)] mod tests { - use super::{ArtifactId, Artifacts}; + use super::{ArtifactId, Artifacts, NODE_VERSION}; use polkadot_primitives::ExecutorParamsHash; use sp_core::H256; use std::{path::Path, str::FromStr}; + fn file_name(code_hash: &str, param_hash: &str) -> String { + format!("wasmtime_polkadot_v{}_0x{}_0x{}", NODE_VERSION, code_hash, param_hash) + } + #[test] fn from_file_name() { assert!(ArtifactId::from_file_name("").is_none()); assert!(ArtifactId::from_file_name("junk").is_none()); + let file_name = file_name( + "0022800000000000000000000000000000000000000000000000000000000000", + "0033900000000000000000000000000000000000000000000000000000000000", + ); + assert_eq!( - ArtifactId::from_file_name( - "wasmtime_0x0022800000000000000000000000000000000000000000000000000000000000_0x0033900000000000000000000000000000000000000000000000000000000000" - ), + ArtifactId::from_file_name(&file_name), Some(ArtifactId::new( hex_literal::hex![ "0022800000000000000000000000000000000000000000000000000000000000" @@ -281,16 +301,19 @@ mod tests { #[test] fn path() { - let path = Path::new("/test"); - let hash = - H256::from_str("1234567890123456789012345678901234567890123456789012345678901234") - .unwrap(); + let dir = Path::new("/test"); + let code_hash = "1234567890123456789012345678901234567890123456789012345678901234"; + let params_hash = "4321098765432109876543210987654321098765432109876543210987654321"; + let file_name = file_name(code_hash, params_hash); + + let code_hash = H256::from_str(code_hash).unwrap(); + let params_hash = H256::from_str(params_hash).unwrap(); assert_eq!( - ArtifactId::new(hash.into(), ExecutorParamsHash::from_hash(hash)).path(path).to_str(), - Some( - "/test/wasmtime_0x1234567890123456789012345678901234567890123456789012345678901234_0x1234567890123456789012345678901234567890123456789012345678901234" - ), + ArtifactId::new(code_hash.into(), ExecutorParamsHash::from_hash(params_hash)) + .path(dir) + .to_str(), + Some(format!("/test/{}", file_name).as_str()), ); } diff --git a/polkadot/node/primitives/src/lib.rs b/polkadot/node/primitives/src/lib.rs index 463c7f960ba08..dab72bb2a5ed8 100644 --- a/polkadot/node/primitives/src/lib.rs +++ b/polkadot/node/primitives/src/lib.rs @@ -53,6 +53,13 @@ pub use disputes::{ ValidDisputeVote, ACTIVE_DURATION_SECS, }; +/// The current node version, which takes the basic SemVer form `<major>.<minor>.<patch>`. +/// In general, minor should be bumped on every release while major or patch releases are +/// relatively rare. +/// +/// The associated worker binaries should use the same version as the node that spawns them. +pub const NODE_VERSION: &'static str = "1.1.0"; + // For a 16-ary Merkle Prefix Trie, we can expect at most 16 32-byte hashes per node // plus some overhead: // header 1 + bitmap 2 + max partial_key 8 + children 16 * (32 + len 1) + value 32 + value len 1 From 1b34571c0c423115813034783ddf524aac257bf5 Mon Sep 17 00:00:00 2001 From: drskalman <35698397+drskalman@users.noreply.github.com> Date: Sun, 15 Oct 2023 05:42:40 -0400 Subject: [PATCH 48/54] Paired-key Crypto Scheme (#1705) BEEFY needs two cryptographic keys at the same time. Validators should sign BEEFY payload using both ECDSA and BLS key. The network will gossip a payload which contains a valid ECDSA key. The prover nodes aggregate the BLS keys if aggregation fails to verifies the validator which provided a valid ECDSA signature but an invalid BLS signature is subject to slashing. As such BEEFY session should be initiated with both key. Currently there is no straight forward way of doing so, beside having a session with RuntimeApp corresponding to a crypto scheme contains both keys. This pull request implement a generic paired_crypto scheme as well as implementing it for (ECDSA, BLS) pair. --------- Co-authored-by: Davide Galassi <davxy@datawok.net> Co-authored-by: Robert Hambrock <roberthambrock@gmail.com> --- substrate/primitives/core/src/bls.rs | 44 +- substrate/primitives/core/src/crypto.rs | 2 +- substrate/primitives/core/src/ecdsa.rs | 54 +- substrate/primitives/core/src/hexdisplay.rs | 2 +- substrate/primitives/core/src/lib.rs | 1 + .../primitives/core/src/paired_crypto.rs | 662 ++++++++++++++++++ 6 files changed, 723 insertions(+), 42 deletions(-) create mode 100644 substrate/primitives/core/src/paired_crypto.rs diff --git a/substrate/primitives/core/src/bls.rs b/substrate/primitives/core/src/bls.rs index 951aa1828ea51..8ce6eb166f868 100644 --- a/substrate/primitives/core/src/bls.rs +++ b/substrate/primitives/core/src/bls.rs @@ -17,7 +17,7 @@ //! Simple BLS (Boneh–Lynn–Shacham) Signature API. -#[cfg(feature = "std")] +#[cfg(feature = "serde")] use crate::crypto::Ss58Codec; use crate::crypto::{ByteArray, CryptoType, Derive, Public as TraitPublic, UncheckedFrom}; #[cfg(feature = "full_crypto")] @@ -28,8 +28,12 @@ use sp_std::vec::Vec; use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; -#[cfg(feature = "std")] + +#[cfg(feature = "serde")] use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +#[cfg(all(not(feature = "std"), feature = "serde"))] +use sp_std::alloc::{format, string::String}; + use w3f_bls::{DoublePublicKey, DoubleSignature, EngineBLS, SerializableToBytes, TinyBLS381}; #[cfg(feature = "full_crypto")] use w3f_bls::{DoublePublicKeyScheme, Keypair, Message, SecretKey}; @@ -39,6 +43,7 @@ use sp_std::{convert::TryFrom, marker::PhantomData, ops::Deref}; /// BLS-377 specialized types pub mod bls377 { + pub use super::{PUBLIC_KEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE}; use crate::crypto::CryptoTypeId; use w3f_bls::TinyBLS377; @@ -60,6 +65,7 @@ pub mod bls377 { /// BLS-381 specialized types pub mod bls381 { + pub use super::{PUBLIC_KEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE}; use crate::crypto::CryptoTypeId; use w3f_bls::TinyBLS381; @@ -83,17 +89,17 @@ trait BlsBound: EngineBLS + HardJunctionId + Send + Sync + 'static {} impl<T: EngineBLS + HardJunctionId + Send + Sync + 'static> BlsBound for T {} -// Secret key serialized size +/// Secret key serialized size #[cfg(feature = "full_crypto")] const SECRET_KEY_SERIALIZED_SIZE: usize = <SecretKey<TinyBLS381> as SerializableToBytes>::SERIALIZED_BYTES_SIZE; -// Public key serialized size -const PUBLIC_KEY_SERIALIZED_SIZE: usize = +/// Public key serialized size +pub const PUBLIC_KEY_SERIALIZED_SIZE: usize = <DoublePublicKey<TinyBLS381> as SerializableToBytes>::SERIALIZED_BYTES_SIZE; -// Signature serialized size -const SIGNATURE_SERIALIZED_SIZE: usize = +/// Signature serialized size +pub const SIGNATURE_SERIALIZED_SIZE: usize = <DoubleSignature<TinyBLS381> as SerializableToBytes>::SERIALIZED_BYTES_SIZE; /// A secret seed. @@ -258,7 +264,7 @@ impl<T> sp_std::fmt::Debug for Public<T> { } } -#[cfg(feature = "std")] +#[cfg(feature = "serde")] impl<T: BlsBound> Serialize for Public<T> { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where @@ -268,7 +274,7 @@ impl<T: BlsBound> Serialize for Public<T> { } } -#[cfg(feature = "std")] +#[cfg(feature = "serde")] impl<'de, T: BlsBound> Deserialize<'de> for Public<T> { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where @@ -317,6 +323,10 @@ impl<T> sp_std::hash::Hash for Signature<T> { } } +impl<T> ByteArray for Signature<T> { + const LEN: usize = SIGNATURE_SERIALIZED_SIZE; +} + impl<T> TryFrom<&[u8]> for Signature<T> { type Error = (); @@ -330,7 +340,7 @@ impl<T> TryFrom<&[u8]> for Signature<T> { } } -#[cfg(feature = "std")] +#[cfg(feature = "serde")] impl<T> Serialize for Signature<T> { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where @@ -340,7 +350,7 @@ impl<T> Serialize for Signature<T> { } } -#[cfg(feature = "std")] +#[cfg(feature = "serde")] impl<'de, T> Deserialize<'de> for Signature<T> { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where @@ -444,10 +454,9 @@ impl<T: BlsBound> TraitPair for Pair<T> { path: Iter, _seed: Option<Seed>, ) -> Result<(Self, Option<Seed>), DeriveError> { - let mut acc: [u8; SECRET_KEY_SERIALIZED_SIZE] = - self.0.secret.to_bytes().try_into().expect( - "Secret key serializer returns a vector of SECRET_KEY_SERIALIZED_SIZE size", - ); + let mut acc: [u8; SECRET_KEY_SERIALIZED_SIZE] = self.0.secret.to_bytes().try_into().expect( + "Secret key serializer returns a vector of SECRET_KEY_SERIALIZED_SIZE size; qed", + ); for j in path { match j { DeriveJunction::Soft(_cc) => return Err(DeriveError::SoftKeyInPath), @@ -529,11 +538,10 @@ mod test { ); } - // Only passes if the seed = (seed mod ScalarField) #[test] fn seed_and_derive_should_work() { let seed = array_bytes::hex2array_unchecked( - "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f00", + "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", ); let pair = Pair::from_seed(&seed); // we are using hash to field so this is not going to work @@ -543,7 +551,7 @@ mod test { assert_eq!( derived.to_raw_vec(), array_bytes::hex2array_unchecked::<_, 32>( - "a4f2269333b3e87c577aa00c4a2cd650b3b30b2e8c286a47c251279ff3a26e0d" + "3a0626d095148813cd1642d38254f1cfff7eb8cc1a2fc83b2a135377c3554c12" ) ); } diff --git a/substrate/primitives/core/src/crypto.rs b/substrate/primitives/core/src/crypto.rs index e1bfb80046f74..ccb61c12f3216 100644 --- a/substrate/primitives/core/src/crypto.rs +++ b/substrate/primitives/core/src/crypto.rs @@ -1213,7 +1213,7 @@ macro_rules! impl_from_entropy_base { [$type; 17], [$type; 18], [$type; 19], [$type; 20], [$type; 21], [$type; 22], [$type; 23], [$type; 24], [$type; 25], [$type; 26], [$type; 27], [$type; 28], [$type; 29], [$type; 30], [$type; 31], [$type; 32], [$type; 36], [$type; 40], [$type; 44], [$type; 48], [$type; 56], [$type; 64], [$type; 72], [$type; 80], - [$type; 96], [$type; 112], [$type; 128], [$type; 160], [$type; 192], [$type; 224], [$type; 256] + [$type; 96], [$type; 112], [$type; 128], [$type; 160], [$type; 177], [$type; 192], [$type; 224], [$type; 256] ); } } diff --git a/substrate/primitives/core/src/ecdsa.rs b/substrate/primitives/core/src/ecdsa.rs index 05bc679386c3d..603fa515a30e8 100644 --- a/substrate/primitives/core/src/ecdsa.rs +++ b/substrate/primitives/core/src/ecdsa.rs @@ -50,6 +50,12 @@ use sp_std::vec::Vec; /// An identifier used to match public keys against ecdsa keys pub const CRYPTO_ID: CryptoTypeId = CryptoTypeId(*b"ecds"); +/// The byte length of public key +pub const PUBLIC_KEY_SERIALIZED_SIZE: usize = 33; + +/// The byte length of signature +pub const SIGNATURE_SERIALIZED_SIZE: usize = 65; + /// A secret seed (which is bytewise essentially equivalent to a SecretKey). /// /// We need it as a different type because `Seed` is expected to be AsRef<[u8]>. @@ -71,11 +77,11 @@ type Seed = [u8; 32]; PartialOrd, Ord, )] -pub struct Public(pub [u8; 33]); +pub struct Public(pub [u8; PUBLIC_KEY_SERIALIZED_SIZE]); impl crate::crypto::FromEntropy for Public { fn from_entropy(input: &mut impl codec::Input) -> Result<Self, codec::Error> { - let mut result = Self([0u8; 33]); + let mut result = Self([0u8; PUBLIC_KEY_SERIALIZED_SIZE]); input.read(&mut result.0[..])?; Ok(result) } @@ -86,7 +92,7 @@ impl Public { /// /// NOTE: No checking goes on to ensure this is a real public key. Only use it if /// you are certain that the array actually is a pubkey. GIGO! - pub fn from_raw(data: [u8; 33]) -> Self { + pub fn from_raw(data: [u8; PUBLIC_KEY_SERIALIZED_SIZE]) -> Self { Self(data) } @@ -109,7 +115,7 @@ impl Public { } impl ByteArray for Public { - const LEN: usize = 33; + const LEN: usize = PUBLIC_KEY_SERIALIZED_SIZE; } impl TraitPublic for Public {} @@ -148,8 +154,8 @@ impl From<Pair> for Public { } } -impl UncheckedFrom<[u8; 33]> for Public { - fn unchecked_from(x: [u8; 33]) -> Self { +impl UncheckedFrom<[u8; PUBLIC_KEY_SERIALIZED_SIZE]> for Public { + fn unchecked_from(x: [u8; PUBLIC_KEY_SERIALIZED_SIZE]) -> Self { Public(x) } } @@ -198,14 +204,18 @@ impl<'de> Deserialize<'de> for Public { /// A signature (a 512-bit value, plus 8 bits for recovery ID). #[cfg_attr(feature = "full_crypto", derive(Hash))] #[derive(Encode, Decode, MaxEncodedLen, PassByInner, TypeInfo, PartialEq, Eq)] -pub struct Signature(pub [u8; 65]); +pub struct Signature(pub [u8; SIGNATURE_SERIALIZED_SIZE]); + +impl ByteArray for Signature { + const LEN: usize = SIGNATURE_SERIALIZED_SIZE; +} impl TryFrom<&[u8]> for Signature { type Error = (); fn try_from(data: &[u8]) -> Result<Self, Self::Error> { - if data.len() == 65 { - let mut inner = [0u8; 65]; + if data.len() == SIGNATURE_SERIALIZED_SIZE { + let mut inner = [0u8; SIGNATURE_SERIALIZED_SIZE]; inner.copy_from_slice(data); Ok(Signature(inner)) } else { @@ -239,7 +249,7 @@ impl<'de> Deserialize<'de> for Signature { impl Clone for Signature { fn clone(&self) -> Self { - let mut r = [0u8; 65]; + let mut r = [0u8; SIGNATURE_SERIALIZED_SIZE]; r.copy_from_slice(&self.0[..]); Signature(r) } @@ -247,18 +257,18 @@ impl Clone for Signature { impl Default for Signature { fn default() -> Self { - Signature([0u8; 65]) + Signature([0u8; SIGNATURE_SERIALIZED_SIZE]) } } -impl From<Signature> for [u8; 65] { - fn from(v: Signature) -> [u8; 65] { +impl From<Signature> for [u8; SIGNATURE_SERIALIZED_SIZE] { + fn from(v: Signature) -> [u8; SIGNATURE_SERIALIZED_SIZE] { v.0 } } -impl AsRef<[u8; 65]> for Signature { - fn as_ref(&self) -> &[u8; 65] { +impl AsRef<[u8; SIGNATURE_SERIALIZED_SIZE]> for Signature { + fn as_ref(&self) -> &[u8; SIGNATURE_SERIALIZED_SIZE] { &self.0 } } @@ -287,8 +297,8 @@ impl sp_std::fmt::Debug for Signature { } } -impl UncheckedFrom<[u8; 65]> for Signature { - fn unchecked_from(data: [u8; 65]) -> Signature { +impl UncheckedFrom<[u8; SIGNATURE_SERIALIZED_SIZE]> for Signature { + fn unchecked_from(data: [u8; SIGNATURE_SERIALIZED_SIZE]) -> Signature { Signature(data) } } @@ -298,7 +308,7 @@ impl Signature { /// /// NOTE: No checking goes on to ensure this is a real signature. Only use it if /// you are certain that the array actually is a signature. GIGO! - pub fn from_raw(data: [u8; 65]) -> Signature { + pub fn from_raw(data: [u8; SIGNATURE_SERIALIZED_SIZE]) -> Signature { Signature(data) } @@ -307,10 +317,10 @@ impl Signature { /// NOTE: No checking goes on to ensure this is a real signature. Only use it if /// you are certain that the array actually is a signature. GIGO! pub fn from_slice(data: &[u8]) -> Option<Self> { - if data.len() != 65 { + if data.len() != SIGNATURE_SERIALIZED_SIZE { return None } - let mut r = [0u8; 65]; + let mut r = [0u8; SIGNATURE_SERIALIZED_SIZE]; r.copy_from_slice(data); Some(Signature(r)) } @@ -473,7 +483,7 @@ impl Pair { pub fn verify_deprecated<M: AsRef<[u8]>>(sig: &Signature, message: M, pubkey: &Public) -> bool { let message = libsecp256k1::Message::parse(&blake2_256(message.as_ref())); - let parse_signature_overflowing = |x: [u8; 65]| { + let parse_signature_overflowing = |x: [u8; SIGNATURE_SERIALIZED_SIZE]| { let sig = libsecp256k1::Signature::parse_overflowing_slice(&x[..64]).ok()?; let rid = libsecp256k1::RecoveryId::parse(x[64]).ok()?; Some((sig, rid)) @@ -726,7 +736,7 @@ mod test { let signature = pair.sign(&message[..]); let serialized_signature = serde_json::to_string(&signature).unwrap(); // Signature is 65 bytes, so 130 chars + 2 quote chars - assert_eq!(serialized_signature.len(), 132); + assert_eq!(serialized_signature.len(), SIGNATURE_SERIALIZED_SIZE * 2 + 2); let signature = serde_json::from_str(&serialized_signature).unwrap(); assert!(Pair::verify(&signature, &message[..], &pair.public())); } diff --git a/substrate/primitives/core/src/hexdisplay.rs b/substrate/primitives/core/src/hexdisplay.rs index 30e045dfc52ac..72bb24a186e54 100644 --- a/substrate/primitives/core/src/hexdisplay.rs +++ b/substrate/primitives/core/src/hexdisplay.rs @@ -96,7 +96,7 @@ macro_rules! impl_non_endians { impl_non_endians!( [u8; 1], [u8; 2], [u8; 3], [u8; 4], [u8; 5], [u8; 6], [u8; 7], [u8; 8], [u8; 10], [u8; 12], [u8; 14], [u8; 16], [u8; 20], [u8; 24], [u8; 28], [u8; 32], [u8; 40], [u8; 48], [u8; 56], - [u8; 64], [u8; 65], [u8; 80], [u8; 96], [u8; 112], [u8; 128], [u8; 144] + [u8; 64], [u8; 65], [u8; 80], [u8; 96], [u8; 112], [u8; 128], [u8; 144], [u8; 177] ); /// Format into ASCII + # + hex, suitable for storage key preimages. diff --git a/substrate/primitives/core/src/lib.rs b/substrate/primitives/core/src/lib.rs index 3a0e1f33f16c9..75b84c89ae68e 100644 --- a/substrate/primitives/core/src/lib.rs +++ b/substrate/primitives/core/src/lib.rs @@ -66,6 +66,7 @@ pub mod hash; #[cfg(feature = "std")] mod hasher; pub mod offchain; +pub mod paired_crypto; pub mod sr25519; pub mod testing; #[cfg(feature = "std")] diff --git a/substrate/primitives/core/src/paired_crypto.rs b/substrate/primitives/core/src/paired_crypto.rs new file mode 100644 index 0000000000000..355fc690779f8 --- /dev/null +++ b/substrate/primitives/core/src/paired_crypto.rs @@ -0,0 +1,662 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// 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. + +//! API for using a pair of crypto schemes together. + +#[cfg(feature = "serde")] +use crate::crypto::Ss58Codec; +use crate::crypto::{ByteArray, CryptoType, Derive, Public as PublicT, UncheckedFrom}; +#[cfg(feature = "full_crypto")] +use crate::crypto::{DeriveError, DeriveJunction, Pair as PairT, SecretStringError}; + +#[cfg(feature = "full_crypto")] +use sp_std::vec::Vec; + +use codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +#[cfg(feature = "serde")] +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +#[cfg(all(not(feature = "std"), feature = "serde"))] +use sp_std::alloc::{format, string::String}; + +use sp_runtime_interface::pass_by::PassByInner; +use sp_std::convert::TryFrom; + +/// ECDSA and BLS12-377 paired crypto scheme +#[cfg(feature = "bls-experimental")] +pub mod ecdsa_n_bls377 { + use crate::{bls377, crypto::CryptoTypeId, ecdsa}; + + /// An identifier used to match public keys against BLS12-377 keys + pub const CRYPTO_ID: CryptoTypeId = CryptoTypeId(*b"ecb7"); + + const PUBLIC_KEY_LEN: usize = + ecdsa::PUBLIC_KEY_SERIALIZED_SIZE + bls377::PUBLIC_KEY_SERIALIZED_SIZE; + const SIGNATURE_LEN: usize = + ecdsa::SIGNATURE_SERIALIZED_SIZE + bls377::SIGNATURE_SERIALIZED_SIZE; + + /// (ECDSA, BLS12-377) key-pair pair. + #[cfg(feature = "full_crypto")] + pub type Pair = super::Pair<ecdsa::Pair, bls377::Pair, PUBLIC_KEY_LEN, SIGNATURE_LEN>; + /// (ECDSA, BLS12-377) public key pair. + pub type Public = super::Public<PUBLIC_KEY_LEN>; + /// (ECDSA, BLS12-377) signature pair. + pub type Signature = super::Signature<SIGNATURE_LEN>; + + impl super::CryptoType for Public { + #[cfg(feature = "full_crypto")] + type Pair = Pair; + } + + impl super::CryptoType for Signature { + #[cfg(feature = "full_crypto")] + type Pair = Pair; + } + + #[cfg(feature = "full_crypto")] + impl super::CryptoType for Pair { + type Pair = Pair; + } +} + +/// Secure seed length. +/// +/// Currently only supporting sub-schemes whose seed is a 32-bytes array. +#[cfg(feature = "full_crypto")] +const SECURE_SEED_LEN: usize = 32; + +/// A secret seed. +/// +/// It's not called a "secret key" because ring doesn't expose the secret keys +/// of the key pair (yeah, dumb); as such we're forced to remember the seed manually if we +/// will need it later (such as for HDKD). +#[cfg(feature = "full_crypto")] +type Seed = [u8; SECURE_SEED_LEN]; + +/// A public key. +#[derive(Clone, Encode, Decode, MaxEncodedLen, TypeInfo, PartialEq, Eq, PartialOrd, Ord)] +pub struct Public<const LEFT_PLUS_RIGHT_LEN: usize>([u8; LEFT_PLUS_RIGHT_LEN]); + +#[cfg(feature = "full_crypto")] +impl<const LEFT_PLUS_RIGHT_LEN: usize> sp_std::hash::Hash for Public<LEFT_PLUS_RIGHT_LEN> { + fn hash<H: sp_std::hash::Hasher>(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> ByteArray for Public<LEFT_PLUS_RIGHT_LEN> { + const LEN: usize = LEFT_PLUS_RIGHT_LEN; +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> TryFrom<&[u8]> for Public<LEFT_PLUS_RIGHT_LEN> { + type Error = (); + + fn try_from(data: &[u8]) -> Result<Self, Self::Error> { + if data.len() != LEFT_PLUS_RIGHT_LEN { + return Err(()) + } + let mut inner = [0u8; LEFT_PLUS_RIGHT_LEN]; + inner.copy_from_slice(data); + Ok(Public(inner)) + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> AsRef<[u8; LEFT_PLUS_RIGHT_LEN]> + for Public<LEFT_PLUS_RIGHT_LEN> +{ + fn as_ref(&self) -> &[u8; LEFT_PLUS_RIGHT_LEN] { + &self.0 + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> AsRef<[u8]> for Public<LEFT_PLUS_RIGHT_LEN> { + fn as_ref(&self) -> &[u8] { + &self.0[..] + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> AsMut<[u8]> for Public<LEFT_PLUS_RIGHT_LEN> { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.0[..] + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> PassByInner for Public<LEFT_PLUS_RIGHT_LEN> { + type Inner = [u8; LEFT_PLUS_RIGHT_LEN]; + + fn into_inner(self) -> Self::Inner { + self.0 + } + + fn inner(&self) -> &Self::Inner { + &self.0 + } + + fn from_inner(inner: Self::Inner) -> Self { + Self(inner) + } +} + +#[cfg(feature = "full_crypto")] +impl< + LeftPair: PairT, + RightPair: PairT, + const LEFT_PLUS_RIGHT_PUBLIC_LEN: usize, + const SIGNATURE_LEN: usize, + > From<Pair<LeftPair, RightPair, LEFT_PLUS_RIGHT_PUBLIC_LEN, SIGNATURE_LEN>> + for Public<LEFT_PLUS_RIGHT_PUBLIC_LEN> +where + Pair<LeftPair, RightPair, LEFT_PLUS_RIGHT_PUBLIC_LEN, SIGNATURE_LEN>: + PairT<Public = Public<LEFT_PLUS_RIGHT_PUBLIC_LEN>>, +{ + fn from(x: Pair<LeftPair, RightPair, LEFT_PLUS_RIGHT_PUBLIC_LEN, SIGNATURE_LEN>) -> Self { + x.public() + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> UncheckedFrom<[u8; LEFT_PLUS_RIGHT_LEN]> + for Public<LEFT_PLUS_RIGHT_LEN> +{ + fn unchecked_from(data: [u8; LEFT_PLUS_RIGHT_LEN]) -> Self { + Public(data) + } +} + +#[cfg(feature = "std")] +impl<const LEFT_PLUS_RIGHT_LEN: usize> std::fmt::Display for Public<LEFT_PLUS_RIGHT_LEN> +where + Public<LEFT_PLUS_RIGHT_LEN>: CryptoType, +{ + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.to_ss58check()) + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> sp_std::fmt::Debug for Public<LEFT_PLUS_RIGHT_LEN> +where + Public<LEFT_PLUS_RIGHT_LEN>: CryptoType, + [u8; LEFT_PLUS_RIGHT_LEN]: crate::hexdisplay::AsBytesRef, +{ + #[cfg(feature = "std")] + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + let s = self.to_ss58check(); + write!(f, "{} ({}...)", crate::hexdisplay::HexDisplay::from(&self.0), &s[0..8]) + } + + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + Ok(()) + } +} + +#[cfg(feature = "serde")] +impl<const LEFT_PLUS_RIGHT_LEN: usize> Serialize for Public<LEFT_PLUS_RIGHT_LEN> +where + Public<LEFT_PLUS_RIGHT_LEN>: CryptoType, +{ + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_str(&self.to_ss58check()) + } +} + +#[cfg(feature = "serde")] +impl<'de, const LEFT_PLUS_RIGHT_LEN: usize> Deserialize<'de> for Public<LEFT_PLUS_RIGHT_LEN> +where + Public<LEFT_PLUS_RIGHT_LEN>: CryptoType, +{ + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + Public::from_ss58check(&String::deserialize(deserializer)?) + .map_err(|e| de::Error::custom(format!("{:?}", e))) + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> PublicT for Public<LEFT_PLUS_RIGHT_LEN> where + Public<LEFT_PLUS_RIGHT_LEN>: CryptoType +{ +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> Derive for Public<LEFT_PLUS_RIGHT_LEN> {} + +/// Trait characterizing a signature which could be used as individual component of an +/// `paired_crypto:Signature` pair. +pub trait SignatureBound: ByteArray {} + +impl<T: ByteArray> SignatureBound for T {} + +/// A pair of signatures of different types +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, PartialEq, Eq)] +pub struct Signature<const LEFT_PLUS_RIGHT_LEN: usize>([u8; LEFT_PLUS_RIGHT_LEN]); + +#[cfg(feature = "full_crypto")] +impl<const LEFT_PLUS_RIGHT_LEN: usize> sp_std::hash::Hash for Signature<LEFT_PLUS_RIGHT_LEN> { + fn hash<H: sp_std::hash::Hasher>(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> ByteArray for Signature<LEFT_PLUS_RIGHT_LEN> { + const LEN: usize = LEFT_PLUS_RIGHT_LEN; +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> TryFrom<&[u8]> for Signature<LEFT_PLUS_RIGHT_LEN> { + type Error = (); + + fn try_from(data: &[u8]) -> Result<Self, Self::Error> { + if data.len() != LEFT_PLUS_RIGHT_LEN { + return Err(()) + } + let mut inner = [0u8; LEFT_PLUS_RIGHT_LEN]; + inner.copy_from_slice(data); + Ok(Signature(inner)) + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> AsMut<[u8]> for Signature<LEFT_PLUS_RIGHT_LEN> { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.0[..] + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> AsRef<[u8; LEFT_PLUS_RIGHT_LEN]> + for Signature<LEFT_PLUS_RIGHT_LEN> +{ + fn as_ref(&self) -> &[u8; LEFT_PLUS_RIGHT_LEN] { + &self.0 + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> AsRef<[u8]> for Signature<LEFT_PLUS_RIGHT_LEN> { + fn as_ref(&self) -> &[u8] { + &self.0[..] + } +} + +#[cfg(feature = "serde")] +impl<const LEFT_PLUS_RIGHT_LEN: usize> Serialize for Signature<LEFT_PLUS_RIGHT_LEN> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_str(&array_bytes::bytes2hex("", self)) + } +} + +#[cfg(feature = "serde")] +impl<'de, const LEFT_PLUS_RIGHT_LEN: usize> Deserialize<'de> for Signature<LEFT_PLUS_RIGHT_LEN> { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let bytes = array_bytes::hex2bytes(&String::deserialize(deserializer)?) + .map_err(|e| de::Error::custom(format!("{:?}", e)))?; + Signature::<LEFT_PLUS_RIGHT_LEN>::try_from(bytes.as_ref()).map_err(|e| { + de::Error::custom(format!("Error converting deserialized data into signature: {:?}", e)) + }) + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> From<Signature<LEFT_PLUS_RIGHT_LEN>> + for [u8; LEFT_PLUS_RIGHT_LEN] +{ + fn from(signature: Signature<LEFT_PLUS_RIGHT_LEN>) -> [u8; LEFT_PLUS_RIGHT_LEN] { + signature.0 + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> sp_std::fmt::Debug for Signature<LEFT_PLUS_RIGHT_LEN> +where + [u8; LEFT_PLUS_RIGHT_LEN]: crate::hexdisplay::AsBytesRef, +{ + #[cfg(feature = "std")] + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "{}", crate::hexdisplay::HexDisplay::from(&self.0)) + } + + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + Ok(()) + } +} + +impl<const LEFT_PLUS_RIGHT_LEN: usize> UncheckedFrom<[u8; LEFT_PLUS_RIGHT_LEN]> + for Signature<LEFT_PLUS_RIGHT_LEN> +{ + fn unchecked_from(data: [u8; LEFT_PLUS_RIGHT_LEN]) -> Self { + Signature(data) + } +} + +/// A key pair. +#[cfg(feature = "full_crypto")] +#[derive(Clone)] +pub struct Pair< + LeftPair: PairT, + RightPair: PairT, + const PUBLIC_KEY_LEN: usize, + const SIGNATURE_LEN: usize, +> { + left: LeftPair, + right: RightPair, +} + +#[cfg(feature = "full_crypto")] +impl< + LeftPair: PairT, + RightPair: PairT, + const PUBLIC_KEY_LEN: usize, + const SIGNATURE_LEN: usize, + > PairT for Pair<LeftPair, RightPair, PUBLIC_KEY_LEN, SIGNATURE_LEN> +where + Pair<LeftPair, RightPair, PUBLIC_KEY_LEN, SIGNATURE_LEN>: CryptoType, + LeftPair::Signature: SignatureBound, + RightPair::Signature: SignatureBound, + Public<PUBLIC_KEY_LEN>: CryptoType, + LeftPair::Seed: From<Seed> + Into<Seed>, + RightPair::Seed: From<Seed> + Into<Seed>, +{ + type Seed = Seed; + type Public = Public<PUBLIC_KEY_LEN>; + type Signature = Signature<SIGNATURE_LEN>; + + fn from_seed_slice(seed_slice: &[u8]) -> Result<Self, SecretStringError> { + if seed_slice.len() != SECURE_SEED_LEN { + return Err(SecretStringError::InvalidSeedLength) + } + let left = LeftPair::from_seed_slice(&seed_slice)?; + let right = RightPair::from_seed_slice(&seed_slice)?; + Ok(Pair { left, right }) + } + + /// Derive a child key from a series of given junctions. + /// + /// Note: if the `LeftPair` and `RightPair` crypto schemes differ in + /// seed derivation, `derive` will drop the seed in the return. + fn derive<Iter: Iterator<Item = DeriveJunction>>( + &self, + path: Iter, + seed: Option<Self::Seed>, + ) -> Result<(Self, Option<Self::Seed>), DeriveError> { + let path: Vec<_> = path.collect(); + + let left = self.left.derive(path.iter().cloned(), seed.map(|s| s.into()))?; + let right = self.right.derive(path.into_iter(), seed.map(|s| s.into()))?; + + let seed = match (left.1, right.1) { + (Some(l), Some(r)) if l.as_ref() == r.as_ref() => Some(l.into()), + _ => None, + }; + + Ok((Self { left: left.0, right: right.0 }, seed)) + } + + fn public(&self) -> Self::Public { + let mut raw = [0u8; PUBLIC_KEY_LEN]; + let left_pub = self.left.public(); + let right_pub = self.right.public(); + raw[..LeftPair::Public::LEN].copy_from_slice(left_pub.as_ref()); + raw[LeftPair::Public::LEN..].copy_from_slice(right_pub.as_ref()); + Self::Public::unchecked_from(raw) + } + + fn sign(&self, message: &[u8]) -> Self::Signature { + let mut raw: [u8; SIGNATURE_LEN] = [0u8; SIGNATURE_LEN]; + raw[..LeftPair::Signature::LEN].copy_from_slice(self.left.sign(message).as_ref()); + raw[LeftPair::Signature::LEN..].copy_from_slice(self.right.sign(message).as_ref()); + Self::Signature::unchecked_from(raw) + } + + fn verify<M: AsRef<[u8]>>(sig: &Self::Signature, message: M, public: &Self::Public) -> bool { + let Ok(left_pub) = public.0[..LeftPair::Public::LEN].try_into() else { return false }; + let Ok(left_sig) = sig.0[0..LeftPair::Signature::LEN].try_into() else { return false }; + if !LeftPair::verify(&left_sig, message.as_ref(), &left_pub) { + return false + } + + let Ok(right_pub) = public.0[LeftPair::Public::LEN..PUBLIC_KEY_LEN].try_into() else { + return false + }; + let Ok(right_sig) = sig.0[LeftPair::Signature::LEN..].try_into() else { return false }; + RightPair::verify(&right_sig, message.as_ref(), &right_pub) + } + + /// Get the seed/secret key for each key and then concatenate them. + fn to_raw_vec(&self) -> Vec<u8> { + let mut raw = self.left.to_raw_vec(); + raw.extend(self.right.to_raw_vec()); + raw + } +} + +// Test set exercising the (ECDSA, BLS12-377) implementation +#[cfg(all(test, feature = "bls-experimental"))] +mod test { + use super::*; + use crate::crypto::DEV_PHRASE; + use ecdsa_n_bls377::{Pair, Signature}; + + use crate::{bls377, ecdsa}; + #[test] + + fn test_length_of_paired_ecdsa_and_bls377_public_key_and_signature_is_correct() { + assert_eq!( + <Pair as PairT>::Public::LEN, + <ecdsa::Pair as PairT>::Public::LEN + <bls377::Pair as PairT>::Public::LEN + ); + assert_eq!( + <Pair as PairT>::Signature::LEN, + <ecdsa::Pair as PairT>::Signature::LEN + <bls377::Pair as PairT>::Signature::LEN + ); + } + + #[test] + fn default_phrase_should_be_used() { + assert_eq!( + Pair::from_string("//Alice///password", None).unwrap().public(), + Pair::from_string(&format!("{}//Alice", DEV_PHRASE), Some("password")) + .unwrap() + .public(), + ); + } + + #[test] + fn seed_and_derive_should_work() { + let seed_for_right_and_left: [u8; SECURE_SEED_LEN] = array_bytes::hex2array_unchecked( + "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + ); + let pair = Pair::from_seed(&seed_for_right_and_left); + // we are using hash to field so this is not going to work + // assert_eq!(pair.seed(), seed); + let path = vec![DeriveJunction::Hard([0u8; 32])]; + let derived = pair.derive(path.into_iter(), None).ok().unwrap().0; + assert_eq!( + derived.to_raw_vec(), + [ + array_bytes::hex2array_unchecked::<&str, SECURE_SEED_LEN>( + "b8eefc4937200a8382d00050e050ced2d4ab72cc2ef1b061477afb51564fdd61" + ), + array_bytes::hex2array_unchecked::<&str, SECURE_SEED_LEN>( + "3a0626d095148813cd1642d38254f1cfff7eb8cc1a2fc83b2a135377c3554c12" + ) + ] + .concat() + ); + } + + #[test] + fn test_vector_should_work() { + let seed_left_and_right: [u8; SECURE_SEED_LEN] = array_bytes::hex2array_unchecked( + "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + ); + let pair = Pair::from_seed(&([seed_left_and_right].concat()[..].try_into().unwrap())); + let public = pair.public(); + assert_eq!( + public, + Public::unchecked_from( + array_bytes::hex2array_unchecked("028db55b05db86c0b1786ca49f095d76344c9e6056b2f02701a7e7f3c20aabfd917a84ca8ce4c37c93c95ecee6a3c0c9a7b9c225093cf2f12dc4f69cbfb847ef9424a18f5755d5a742247d386ff2aabb806bcf160eff31293ea9616976628f77266c8a8cc1d8753be04197bd6cdd8c5c87a148f782c4c1568d599b48833fd539001e580cff64bbc71850605433fcd051f3afc3b74819786f815ffb5272030a8d03e5df61e6183f8fd8ea85f26defa83400"), + ), + ); + let message = b""; + let signature = + array_bytes::hex2array_unchecked("3dde91174bd9359027be59a428b8146513df80a2a3c7eda2194f64de04a69ab97b753169e94db6ffd50921a2668a48b94ca11e3d32c1ff19cfe88890aa7e8f3c00d1e3013161991e142d8751017d4996209c2ff8a9ee160f373733eda3b4b785ba6edce9f45f87104bbe07aa6aa6eb2780aa705efb2c13d3b317d6409d159d23bdc7cdd5c2a832d1551cf49d811d49c901495e527dbd532e3a462335ce2686009104aba7bc11c5b22be78f3198d2727a0b" + ); + let signature = Signature::unchecked_from(signature); + assert!(pair.sign(&message[..]) == signature); + assert!(Pair::verify(&signature, &message[..], &public)); + } + + #[test] + fn test_vector_by_string_should_work() { + let pair = Pair::from_string( + "0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + None, + ) + .unwrap(); + let public = pair.public(); + assert_eq!( + public, + Public::unchecked_from( + array_bytes::hex2array_unchecked("028db55b05db86c0b1786ca49f095d76344c9e6056b2f02701a7e7f3c20aabfd916dc6be608fab3c6bd894a606be86db346cc170db85c733853a371f3db54ae1b12052c0888d472760c81b537572a26f00db865e5963aef8634f9917571c51b538b564b2a9ceda938c8b930969ee3b832448e08e33a79e9ddd28af419a3ce45300f5dbc768b067781f44f3fe05a19e6b07b1c4196151ec3f8ea37e4f89a8963030d2101e931276bb9ebe1f20102239d780" + ), + ), + ); + let message = b""; + let signature = + array_bytes::hex2array_unchecked("3dde91174bd9359027be59a428b8146513df80a2a3c7eda2194f64de04a69ab97b753169e94db6ffd50921a2668a48b94ca11e3d32c1ff19cfe88890aa7e8f3c00bbb395bbdee1a35930912034f5fde3b36df2835a0536c865501b0675776a1d5931a3bea2e66eff73b2546c6af2061a8019223e4ebbbed661b2538e0f5823f2c708eb89c406beca8fcb53a5c13dbc7c0c42e4cf2be2942bba96ea29297915a06bd2b1b979c0e2ac8fd4ec684a6b5d110c" + ); + let signature = Signature::unchecked_from(signature); + assert!(pair.sign(&message[..]) == signature); + assert!(Pair::verify(&signature, &message[..], &public)); + } + + #[test] + fn generated_pair_should_work() { + let (pair, _) = Pair::generate(); + let public = pair.public(); + let message = b"Something important"; + let signature = pair.sign(&message[..]); + assert!(Pair::verify(&signature, &message[..], &public)); + assert!(!Pair::verify(&signature, b"Something else", &public)); + } + + #[test] + fn seeded_pair_should_work() { + let pair = + Pair::from_seed(&(b"12345678901234567890123456789012".as_slice().try_into().unwrap())); + let public = pair.public(); + assert_eq!( + public, + Public::unchecked_from( + array_bytes::hex2array_unchecked("035676109c54b9a16d271abeb4954316a40a32bcce023ac14c8e26e958aa68fba9754d2f2bbfa67df54d7e0e951979a18a1e0f45948857752cc2bac6bbb0b1d05e8e48bcc453920bf0c4bbd5993212480112a1fb433f04d74af0a8b700d93dc957ab3207f8d071e948f5aca1a7632c00bdf6d06be05b43e2e6216dccc8a5d55a0071cb2313cfd60b7e9114619cd17c06843b352f0b607a99122f6651df8f02e1ad3697bd208e62af047ddd7b942ba80080") + ), + ); + let message = + array_bytes::hex2bytes_unchecked("2f8c6129d816cf51c374bc7f08c3e63ed156cf78aefb4a6550d97b87997977ee00000000000000000200d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a4500000000000000" + ); + let signature = pair.sign(&message[..]); + println!("Correct signature: {:?}", signature); + assert!(Pair::verify(&signature, &message[..], &public)); + assert!(!Pair::verify(&signature, "Other message", &public)); + } + + #[test] + fn generate_with_phrase_recovery_possible() { + let (pair1, phrase, _) = Pair::generate_with_phrase(None); + let (pair2, _) = Pair::from_phrase(&phrase, None).unwrap(); + + assert_eq!(pair1.public(), pair2.public()); + } + + #[test] + fn generate_with_password_phrase_recovery_possible() { + let (pair1, phrase, _) = Pair::generate_with_phrase(Some("password")); + let (pair2, _) = Pair::from_phrase(&phrase, Some("password")).unwrap(); + + assert_eq!(pair1.public(), pair2.public()); + } + + #[test] + fn password_does_something() { + let (pair1, phrase, _) = Pair::generate_with_phrase(Some("password")); + let (pair2, _) = Pair::from_phrase(&phrase, None).unwrap(); + + assert_ne!(pair1.public(), pair2.public()); + } + + #[test] + fn ss58check_roundtrip_works() { + let pair = + Pair::from_seed(&(b"12345678901234567890123456789012".as_slice().try_into().unwrap())); + let public = pair.public(); + let s = public.to_ss58check(); + println!("Correct: {}", s); + let cmp = Public::from_ss58check(&s).unwrap(); + assert_eq!(cmp, public); + } + + #[test] + fn signature_serialization_works() { + let pair = + Pair::from_seed(&(b"12345678901234567890123456789012".as_slice().try_into().unwrap())); + let message = b"Something important"; + let signature = pair.sign(&message[..]); + + let serialized_signature = serde_json::to_string(&signature).unwrap(); + println!("{:?} -- {:}", signature.0, serialized_signature); + // Signature is 177 bytes, hexify * 2 + 2 quote charsy + assert_eq!(serialized_signature.len(), 356); + let signature = serde_json::from_str(&serialized_signature).unwrap(); + assert!(Pair::verify(&signature, &message[..], &pair.public())); + } + + #[test] + fn signature_serialization_doesnt_panic() { + fn deserialize_signature(text: &str) -> Result<Signature, serde_json::error::Error> { + serde_json::from_str(text) + } + assert!(deserialize_signature("Not valid json.").is_err()); + assert!(deserialize_signature("\"Not an actual signature.\"").is_err()); + // Poorly-sized + assert!(deserialize_signature("\"abc123\"").is_err()); + } + + #[test] + fn encode_and_decode_public_key_works() { + let pair = + Pair::from_seed(&(b"12345678901234567890123456789012".as_slice().try_into().unwrap())); + let public = pair.public(); + let encoded_public = public.encode(); + let decoded_public = Public::decode(&mut encoded_public.as_slice()).unwrap(); + assert_eq!(public, decoded_public) + } + + #[test] + fn encode_and_decode_signature_works() { + let pair = + Pair::from_seed(&(b"12345678901234567890123456789012".as_slice().try_into().unwrap())); + let message = b"Something important"; + let signature = pair.sign(&message[..]); + let encoded_signature = signature.encode(); + let decoded_signature = Signature::decode(&mut encoded_signature.as_slice()).unwrap(); + assert_eq!(signature, decoded_signature) + } +} From 8ee4042c3bc86d4232e51a9bdb5ec1ae5ca444ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Pestana?= <g6pestana@gmail.com> Date: Sun, 15 Oct 2023 22:50:07 +0200 Subject: [PATCH 49/54] Refactor staking ledger (#1484) This PR refactors the staking ledger logic to encapsulate all reads and mutations of `Ledger`, `Bonded`, `Payee` and stake locks within the `StakingLedger` struct implementation. With these changes, all the reads and mutations to the `Ledger`, `Payee` and `Bonded` storage map should be done through the methods exposed by StakingLedger to ensure the data and lock consistency of the operations. The new introduced methods that mutate and read Ledger are: - `ledger.update()`: inserts/updates a staking ledger in storage; updates staking locks accordingly (and ledger.bond(), which is synthatic sugar for ledger.update()) - `ledger.kill()`: removes all Bonded and StakingLedger related data for a given ledger; updates staking locks accordingly; `StakingLedger::get(account)`: queries both the `Bonded` and `Ledger` storages and returns a `Option<StakingLedger>`. The pallet impl exposes fn ledger(account) as synthatic sugar for `StakingLedger::get(account)`. Retrieving a ledger with `StakingLedger::get()` can be done by providing either a stash or controller account. The input must be wrapped in a `StakingAccount` variant (Stash or Controller) which is treated accordingly. This simplifies the caller API but will eventually be deprecated once we completely get rid of the controller account in staking. However, this refactor will help with the work necessary when completely removing the controller. Other goals: - No logical changes have been introduced in this PR; - No breaking changes or updates in wallets required; - No new storage items or need to perform storage migrations; - Centralise the changes to bonds and ledger updates to simplify the OnStakingUpdate updates to the target list (related to https://github.com/paritytech/polkadot-sdk/issues/443) Note: it would be great to prevent or at least raise a warning if `Ledger<T>`, `Payee<T>` and `Bonded<T>` storage types are accessed outside the `StakingLedger` implementation. This PR should not get blocked by that feature, but there's a tracking issue here https://github.com/paritytech/polkadot-sdk/issues/149 Related and step towards https://github.com/paritytech/polkadot-sdk/issues/443 --- .../src/disputes/slashing/benchmarking.rs | 2 +- .../test-staking-e2e/src/lib.rs | 10 +- .../frame/session/benchmarking/src/lib.rs | 2 +- substrate/frame/staking/src/benchmarking.rs | 14 +- substrate/frame/staking/src/ledger.rs | 259 +++++++++ substrate/frame/staking/src/lib.rs | 40 +- substrate/frame/staking/src/mock.rs | 17 +- substrate/frame/staking/src/pallet/impls.rs | 177 +++---- substrate/frame/staking/src/pallet/mod.rs | 145 +++-- substrate/frame/staking/src/slashing.rs | 18 +- substrate/frame/staking/src/tests.rs | 500 +++++++++++------- substrate/primitives/staking/src/lib.rs | 17 + 12 files changed, 790 insertions(+), 411 deletions(-) create mode 100644 substrate/frame/staking/src/ledger.rs diff --git a/polkadot/runtime/parachains/src/disputes/slashing/benchmarking.rs b/polkadot/runtime/parachains/src/disputes/slashing/benchmarking.rs index 3ede1c9088025..f075ce5ca737d 100644 --- a/polkadot/runtime/parachains/src/disputes/slashing/benchmarking.rs +++ b/polkadot/runtime/parachains/src/disputes/slashing/benchmarking.rs @@ -51,7 +51,7 @@ where use rand::{RngCore, SeedableRng}; let validator = T::Lookup::lookup(who).unwrap(); - let controller = pallet_staking::Pallet::<T>::bonded(validator).unwrap(); + let controller = pallet_staking::Pallet::<T>::bonded(&validator).unwrap(); let keys = { const SESSION_KEY_LEN: usize = 32; diff --git a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs index e40bac3e9fc44..1d3f4712b1d60 100644 --- a/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs +++ b/substrate/frame/election-provider-multi-phase/test-staking-e2e/src/lib.rs @@ -338,7 +338,7 @@ fn ledger_consistency_active_balance_below_ed() { ExtBuilder::default().staking(StakingExtBuilder::default()).build_offchainify(); ext.execute_with(|| { - assert_eq!(Staking::ledger(&11).unwrap().active, 1000); + assert_eq!(Staking::ledger(11.into()).unwrap().active, 1000); // unbonding total of active stake fails because the active ledger balance would fall // below the `MinNominatorBond`. @@ -356,13 +356,13 @@ fn ledger_consistency_active_balance_below_ed() { // the active balance of the ledger entry is 0, while total balance is 1000 until // `withdraw_unbonded` is called. - assert_eq!(Staking::ledger(&11).unwrap().active, 0); - assert_eq!(Staking::ledger(&11).unwrap().total, 1000); + assert_eq!(Staking::ledger(11.into()).unwrap().active, 0); + assert_eq!(Staking::ledger(11.into()).unwrap().total, 1000); // trying to withdraw the unbonded balance won't work yet because not enough bonding // eras have passed. assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(11), 0)); - assert_eq!(Staking::ledger(&11).unwrap().total, 1000); + assert_eq!(Staking::ledger(11.into()).unwrap().total, 1000); // tries to reap stash after chilling, which fails since the stash total balance is // above ED. @@ -384,6 +384,6 @@ fn ledger_consistency_active_balance_below_ed() { pool_state, ); assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(11), 0)); - assert_eq!(Staking::ledger(&11), None); + assert!(Staking::ledger(11.into()).is_err()); }); } diff --git a/substrate/frame/session/benchmarking/src/lib.rs b/substrate/frame/session/benchmarking/src/lib.rs index 722bb14fb56ed..84258d84994f4 100644 --- a/substrate/frame/session/benchmarking/src/lib.rs +++ b/substrate/frame/session/benchmarking/src/lib.rs @@ -134,7 +134,7 @@ fn check_membership_proof_setup<T: Config>( use rand::{RngCore, SeedableRng}; let validator = T::Lookup::lookup(who).unwrap(); - let controller = pallet_staking::Pallet::<T>::bonded(validator).unwrap(); + let controller = pallet_staking::Pallet::<T>::bonded(&validator).unwrap(); let keys = { let mut keys = [0u8; 128]; diff --git a/substrate/frame/staking/src/benchmarking.rs b/substrate/frame/staking/src/benchmarking.rs index ce5c35524c747..f94d9bf4b328a 100644 --- a/substrate/frame/staking/src/benchmarking.rs +++ b/substrate/frame/staking/src/benchmarking.rs @@ -29,7 +29,7 @@ use frame_support::{ }; use sp_runtime::{ traits::{Bounded, One, StaticLookup, TrailingZeroInput, Zero}, - Perbill, Percent, + Perbill, Percent, Saturating, }; use sp_staking::{currency_to_vote::CurrencyToVote, SessionIndex}; use sp_std::prelude::*; @@ -684,13 +684,11 @@ benchmarks! { let stash = scenario.origin_stash1; add_slashing_spans::<T>(&stash, s); - let l = StakingLedger { - stash: stash.clone(), - active: T::Currency::minimum_balance() - One::one(), - total: T::Currency::minimum_balance() - One::one(), - unlocking: Default::default(), - claimed_rewards: Default::default(), - }; + let l = StakingLedger::<T>::new( + stash.clone(), + T::Currency::minimum_balance() - One::one(), + Default::default(), + ); Ledger::<T>::insert(&controller, l); assert!(Bonded::<T>::contains_key(&stash)); diff --git a/substrate/frame/staking/src/ledger.rs b/substrate/frame/staking/src/ledger.rs new file mode 100644 index 0000000000000..cf9b4635bf55a --- /dev/null +++ b/substrate/frame/staking/src/ledger.rs @@ -0,0 +1,259 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// 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. + +//! A Ledger implementation for stakers. +//! +//! A [`StakingLedger`] encapsulates all the state and logic related to the stake of bonded +//! stakers, namely, it handles the following storage items: +//! * [`Bonded`]: mutates and reads the state of the controller <> stash bond map (to be deprecated +//! soon); +//! * [`Ledger`]: mutates and reads the state of all the stakers. The [`Ledger`] storage item stores +//! instances of [`StakingLedger`] keyed by the staker's controller account and should be mutated +//! and read through the [`StakingLedger`] API; +//! * [`Payee`]: mutates and reads the reward destination preferences for a bonded stash. +//! * Staking locks: mutates the locks for staking. +//! +//! NOTE: All the storage operations related to the staking ledger (both reads and writes) *MUST* be +//! performed through the methods exposed by the [`StakingLedger`] implementation in order to ensure +//! state consistency. + +use frame_support::{ + defensive, + traits::{LockableCurrency, WithdrawReasons}, + BoundedVec, +}; +use sp_staking::{EraIndex, StakingAccount}; +use sp_std::prelude::*; + +use crate::{ + BalanceOf, Bonded, Config, Error, Ledger, Payee, RewardDestination, StakingLedger, STAKING_ID, +}; + +#[cfg(any(feature = "runtime-benchmarks", test))] +use sp_runtime::traits::Zero; + +impl<T: Config> StakingLedger<T> { + #[cfg(any(feature = "runtime-benchmarks", test))] + pub fn default_from(stash: T::AccountId) -> Self { + Self { + stash: stash.clone(), + total: Zero::zero(), + active: Zero::zero(), + unlocking: Default::default(), + claimed_rewards: Default::default(), + controller: Some(stash), + } + } + + /// Returns a new instance of a staking ledger. + /// + /// The [`Ledger`] storage is not mutated. In order to store, `StakingLedger::update` must be + /// called on the returned staking ledger. + /// + /// Note: as the controller accounts are being deprecated, the stash account is the same as the + /// controller account. + pub fn new( + stash: T::AccountId, + stake: BalanceOf<T>, + claimed_rewards: BoundedVec<EraIndex, T::HistoryDepth>, + ) -> Self { + Self { + stash: stash.clone(), + active: stake, + total: stake, + unlocking: Default::default(), + claimed_rewards, + // controllers are deprecated and mapped 1-1 to stashes. + controller: Some(stash), + } + } + + /// Returns the paired account, if any. + /// + /// A "pair" refers to the tuple (stash, controller). If the input is a + /// [`StakingAccount::Stash`] variant, its pair account will be of type + /// [`StakingAccount::Controller`] and vice-versa. + /// + /// This method is meant to abstract from the runtime development the difference between stash + /// and controller. This will be deprecated once the controller is fully deprecated as well. + pub(crate) fn paired_account(account: StakingAccount<T::AccountId>) -> Option<T::AccountId> { + match account { + StakingAccount::Stash(stash) => <Bonded<T>>::get(stash), + StakingAccount::Controller(controller) => + <Ledger<T>>::get(&controller).map(|ledger| ledger.stash), + } + } + + /// Returns whether a given account is bonded. + pub(crate) fn is_bonded(account: StakingAccount<T::AccountId>) -> bool { + match account { + StakingAccount::Stash(stash) => <Bonded<T>>::contains_key(stash), + StakingAccount::Controller(controller) => <Ledger<T>>::contains_key(controller), + } + } + + /// Returns a staking ledger, if it is bonded and it exists in storage. + /// + /// This getter can be called with either a controller or stash account, provided that the + /// account is properly wrapped in the respective [`StakingAccount`] variant. This is meant to + /// abstract the concept of controller/stash accounts from the caller. + pub(crate) fn get(account: StakingAccount<T::AccountId>) -> Result<StakingLedger<T>, Error<T>> { + let controller = match account { + StakingAccount::Stash(stash) => <Bonded<T>>::get(stash).ok_or(Error::<T>::NotStash), + StakingAccount::Controller(controller) => Ok(controller), + }?; + + <Ledger<T>>::get(&controller) + .map(|mut ledger| { + ledger.controller = Some(controller.clone()); + ledger + }) + .ok_or(Error::<T>::NotController) + } + + /// Returns the reward destination of a staking ledger, stored in [`Payee`]. + /// + /// Note: if the stash is not bonded and/or does not have an entry in [`Payee`], it returns the + /// default reward destination. + pub(crate) fn reward_destination( + account: StakingAccount<T::AccountId>, + ) -> RewardDestination<T::AccountId> { + let stash = match account { + StakingAccount::Stash(stash) => Some(stash), + StakingAccount::Controller(controller) => + Self::paired_account(StakingAccount::Controller(controller)), + }; + + if let Some(stash) = stash { + <Payee<T>>::get(stash) + } else { + defensive!("fetched reward destination from unbonded stash {}", stash); + RewardDestination::default() + } + } + + /// Returns the controller account of a staking ledger. + /// + /// Note: it will fallback into querying the [`Bonded`] storage with the ledger stash if the + /// controller is not set in `self`, which most likely means that self was fetched directly from + /// [`Ledger`] instead of through the methods exposed in [`StakingLedger`]. If the ledger does + /// not exist in storage, it returns `None`. + pub(crate) fn controller(&self) -> Option<T::AccountId> { + self.controller.clone().or_else(|| { + defensive!("fetched a controller on a ledger instance without it."); + Self::paired_account(StakingAccount::Stash(self.stash.clone())) + }) + } + + /// Inserts/updates a staking ledger account. + /// + /// Bonds the ledger if it is not bonded yet, signalling that this is a new ledger. The staking + /// locks of the stash account are updated accordingly. + /// + /// Note: To ensure lock consistency, all the [`Ledger`] storage updates should be made through + /// this helper function. + pub(crate) fn update(self) -> Result<(), Error<T>> { + if !<Bonded<T>>::contains_key(&self.stash) { + return Err(Error::<T>::NotStash) + } + + T::Currency::set_lock(STAKING_ID, &self.stash, self.total, WithdrawReasons::all()); + Ledger::<T>::insert( + &self.controller().ok_or_else(|| { + defensive!("update called on a ledger that is not bonded."); + Error::<T>::NotController + })?, + &self, + ); + + Ok(()) + } + + /// Bonds a ledger. + /// + /// It sets the reward preferences for the bonded stash. + pub(crate) fn bond(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> { + if <Bonded<T>>::contains_key(&self.stash) { + Err(Error::<T>::AlreadyBonded) + } else { + <Payee<T>>::insert(&self.stash, payee); + <Bonded<T>>::insert(&self.stash, &self.stash); + self.update() + } + } + + /// Sets the ledger Payee. + pub(crate) fn set_payee(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> { + if !<Bonded<T>>::contains_key(&self.stash) { + Err(Error::<T>::NotStash) + } else { + <Payee<T>>::insert(&self.stash, payee); + Ok(()) + } + } + + /// Clears all data related to a staking ledger and its bond in both [`Ledger`] and [`Bonded`] + /// storage items and updates the stash staking lock. + pub(crate) fn kill(stash: &T::AccountId) -> Result<(), Error<T>> { + let controller = <Bonded<T>>::get(stash).ok_or(Error::<T>::NotStash)?; + + <Ledger<T>>::get(&controller).ok_or(Error::<T>::NotController).map(|ledger| { + T::Currency::remove_lock(STAKING_ID, &ledger.stash); + Ledger::<T>::remove(controller); + + <Bonded<T>>::remove(&stash); + <Payee<T>>::remove(&stash); + + Ok(()) + })? + } +} + +#[cfg(test)] +use { + crate::UnlockChunk, + codec::{Decode, Encode, MaxEncodedLen}, + scale_info::TypeInfo, +}; + +// This structs makes it easy to write tests to compare staking ledgers fetched from storage. This +// is required because the controller field is not stored in storage and it is private. +#[cfg(test)] +#[derive(frame_support::DebugNoBound, Clone, Encode, Decode, TypeInfo, MaxEncodedLen)] +pub struct StakingLedgerInspect<T: Config> { + pub stash: T::AccountId, + #[codec(compact)] + pub total: BalanceOf<T>, + #[codec(compact)] + pub active: BalanceOf<T>, + pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>, + pub claimed_rewards: BoundedVec<EraIndex, T::HistoryDepth>, +} + +#[cfg(test)] +impl<T: Config> PartialEq<StakingLedgerInspect<T>> for StakingLedger<T> { + fn eq(&self, other: &StakingLedgerInspect<T>) -> bool { + self.stash == other.stash && + self.total == other.total && + self.active == other.active && + self.unlocking == other.unlocking && + self.claimed_rewards == other.claimed_rewards + } +} + +#[cfg(test)] +impl<T: Config> codec::EncodeLike<StakingLedger<T>> for StakingLedgerInspect<T> {} diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index dcf57a4643233..227326763a9bd 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -91,7 +91,7 @@ //! #### Nomination //! //! A **nominator** does not take any _direct_ role in maintaining the network, instead, it votes on -//! a set of validators to be elected. Once interest in nomination is stated by an account, it +//! a set of validators to be elected. Once interest in nomination is stated by an account, it //! takes effect at the next election round. The funds in the nominator's stash account indicate the //! _weight_ of its vote. Both the rewards and any punishment that a validator earns are shared //! between the validator and its nominators. This rule incentivizes the nominators to NOT vote for @@ -294,6 +294,7 @@ mod tests; pub mod election_size_tracker; pub mod inflation; +pub mod ledger; pub mod migrations; pub mod slashing; pub mod weights; @@ -302,26 +303,27 @@ mod pallet; use codec::{Decode, Encode, HasCompact, MaxEncodedLen}; use frame_support::{ - traits::{ConstU32, Currency, Defensive, Get}, + traits::{ConstU32, Currency, Defensive, Get, LockIdentifier}, weights::Weight, BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, }; use scale_info::TypeInfo; use sp_runtime::{ curve::PiecewiseLinear, - traits::{AtLeast32BitUnsigned, Convert, Saturating, StaticLookup, Zero}, - Perbill, Perquintill, Rounding, RuntimeDebug, + traits::{AtLeast32BitUnsigned, Convert, StaticLookup, Zero}, + Perbill, Perquintill, Rounding, RuntimeDebug, Saturating, }; pub use sp_staking::StakerStatus; use sp_staking::{ offence::{Offence, OffenceError, ReportOffence}, - EraIndex, OnStakingUpdate, SessionIndex, + EraIndex, OnStakingUpdate, SessionIndex, StakingAccount, }; use sp_std::{collections::btree_map::BTreeMap, prelude::*}; pub use weights::WeightInfo; pub use pallet::{pallet::*, UseNominatorsAndValidatorsMap, UseValidatorsMap}; +pub(crate) const STAKING_ID: LockIdentifier = *b"staking "; pub(crate) const LOG_TARGET: &str = "runtime::staking"; // syntactic sugar for logging. @@ -433,6 +435,14 @@ pub struct UnlockChunk<Balance: HasCompact + MaxEncodedLen> { } /// The ledger of a (bonded) stash. +/// +/// Note: All the reads and mutations to the [`Ledger`], [`Bonded`] and [`Payee`] storage items +/// *MUST* be performed through the methods exposed by this struct, to ensure the consistency of +/// ledger's data and corresponding staking lock +/// +/// TODO: move struct definition and full implementation into `/src/ledger.rs`. Currently +/// leaving here to enforce a clean PR diff, given how critical this logic is. Tracking issue +/// <https://github.com/paritytech/substrate/issues/14749>. #[derive( PartialEqNoBound, EqNoBound, @@ -462,20 +472,15 @@ pub struct StakingLedger<T: Config> { /// List of eras for which the stakers behind a validator have claimed rewards. Only updated /// for validators. pub claimed_rewards: BoundedVec<EraIndex, T::HistoryDepth>, + /// The controller associated with this ledger's stash. + /// + /// This is not stored on-chain, and is only bundled when the ledger is read from storage. + /// Use [`controller`] function to get the controller associated with the ledger. + #[codec(skip)] + controller: Option<T::AccountId>, } impl<T: Config> StakingLedger<T> { - /// Initializes the default object using the given `validator`. - pub fn default_from(stash: T::AccountId) -> Self { - Self { - stash, - total: Zero::zero(), - active: Zero::zero(), - unlocking: Default::default(), - claimed_rewards: Default::default(), - } - } - /// Remove entries from `unlocking` that are sufficiently old and reduce the /// total by the sum of their balances. fn consolidate_unlocked(self, current_era: EraIndex) -> Self { @@ -503,6 +508,7 @@ impl<T: Config> StakingLedger<T> { active: self.active, unlocking, claimed_rewards: self.claimed_rewards, + controller: self.controller, } } @@ -927,7 +933,7 @@ pub struct StashOf<T>(sp_std::marker::PhantomData<T>); impl<T: Config> Convert<T::AccountId, Option<T::AccountId>> for StashOf<T> { fn convert(controller: T::AccountId) -> Option<T::AccountId> { - <Pallet<T>>::ledger(&controller).map(|l| l.stash) + StakingLedger::<T>::paired_account(StakingAccount::Controller(controller)) } } diff --git a/substrate/frame/staking/src/mock.rs b/substrate/frame/staking/src/mock.rs index c41144278f2c1..26d05d3a8c878 100644 --- a/substrate/frame/staking/src/mock.rs +++ b/substrate/frame/staking/src/mock.rs @@ -39,7 +39,10 @@ use sp_runtime::{ traits::{IdentityLookup, Zero}, BuildStorage, }; -use sp_staking::offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}; +use sp_staking::{ + offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, + OnStakingUpdate, +}; pub const INIT_TIMESTAMP: u64 = 30_000; pub const BLOCK_TIME: u64 = 1000; @@ -77,7 +80,7 @@ impl sp_runtime::BoundToRuntimeAppPublic for OtherSessionHandler { } pub fn is_disabled(controller: AccountId) -> bool { - let stash = Staking::ledger(&controller).unwrap().stash; + let stash = Ledger::<Test>::get(&controller).unwrap().stash; let validator_index = match Session::validators().iter().position(|v| *v == stash) { Some(index) => index as u32, None => return false, @@ -778,6 +781,16 @@ pub(crate) fn make_all_reward_payment(era: EraIndex) { } } +pub(crate) fn bond_controller_stash(controller: AccountId, stash: AccountId) -> Result<(), String> { + <Bonded<Test>>::get(&stash).map_or(Ok(()), |_| Err("stash already bonded"))?; + <Ledger<Test>>::get(&controller).map_or(Ok(()), |_| Err("controller already bonded"))?; + + <Bonded<Test>>::insert(stash, controller); + <Ledger<Test>>::insert(controller, StakingLedger::<Test>::default_from(stash)); + + Ok(()) +} + #[macro_export] macro_rules! assert_session_era { ($session:expr, $era:expr) => { diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 3d9b1157ca87a..ad2de1d59315a 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -27,8 +27,8 @@ use frame_support::{ dispatch::WithPostDispatchInfo, pallet_prelude::*, traits::{ - Currency, Defensive, DefensiveResult, EstimateNextNewSession, Get, Imbalance, - LockableCurrency, OnUnbalanced, TryCollect, UnixTime, WithdrawReasons, + Currency, Defensive, DefensiveResult, EstimateNextNewSession, Get, Imbalance, OnUnbalanced, + TryCollect, UnixTime, }, weights::Weight, }; @@ -41,7 +41,9 @@ use sp_runtime::{ use sp_staking::{ currency_to_vote::CurrencyToVote, offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, - EraIndex, SessionIndex, Stake, StakingInterface, + EraIndex, SessionIndex, Stake, + StakingAccount::{self, Controller, Stash}, + StakingInterface, }; use sp_std::prelude::*; @@ -52,7 +54,7 @@ use crate::{ SessionInterface, StakingLedger, ValidatorPrefs, }; -use super::{pallet::*, STAKING_ID}; +use super::pallet::*; #[cfg(feature = "try-runtime")] use frame_support::ensure; @@ -68,10 +70,24 @@ use sp_runtime::TryRuntimeError; const NPOS_MAX_ITERATIONS_COEFFICIENT: u32 = 2; impl<T: Config> Pallet<T> { + /// Fetches the ledger associated with a controller or stash account, if any. + pub fn ledger(account: StakingAccount<T::AccountId>) -> Result<StakingLedger<T>, Error<T>> { + StakingLedger::<T>::get(account) + } + + pub fn payee(account: StakingAccount<T::AccountId>) -> RewardDestination<T::AccountId> { + StakingLedger::<T>::reward_destination(account) + } + + /// Fetches the controller bonded to a stash account, if any. + pub fn bonded(stash: &T::AccountId) -> Option<T::AccountId> { + StakingLedger::<T>::paired_account(Stash(stash.clone())) + } + /// The total balance that can be slashed from a stash account as of right now. pub fn slashable_balance_of(stash: &T::AccountId) -> BalanceOf<T> { // Weight note: consider making the stake accessible through stash. - Self::bonded(stash).and_then(Self::ledger).map(|l| l.active).unwrap_or_default() + Self::ledger(Stash(stash.clone())).map(|l| l.active).unwrap_or_default() } /// Internal impl of [`Self::slashable_balance_of`] that returns [`VoteWeight`]. @@ -105,25 +121,24 @@ impl<T: Config> Pallet<T> { controller: &T::AccountId, num_slashing_spans: u32, ) -> Result<Weight, DispatchError> { - let mut ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?; + let mut ledger = Self::ledger(Controller(controller.clone()))?; let (stash, old_total) = (ledger.stash.clone(), ledger.total); if let Some(current_era) = Self::current_era() { ledger = ledger.consolidate_unlocked(current_era) } + let new_total = ledger.total; let used_weight = if ledger.unlocking.is_empty() && ledger.active < T::Currency::minimum_balance() { // This account must have called `unbond()` with some value that caused the active // portion to fall below existential deposit + will have no more unlocking chunks // left. We can now safely remove all staking-related information. - Self::kill_stash(&stash, num_slashing_spans)?; - // Remove the lock. - T::Currency::remove_lock(STAKING_ID, &stash); + Self::kill_stash(&ledger.stash, num_slashing_spans)?; T::WeightInfo::withdraw_unbonded_kill(num_slashing_spans) } else { // This was the consequence of a partial unbond. just update the ledger and move on. - Self::update_ledger(&controller, &ledger); + ledger.update()?; // This is only an update, so we use less overall weight. T::WeightInfo::withdraw_unbonded_update(num_slashing_spans) @@ -131,9 +146,9 @@ impl<T: Config> Pallet<T> { // `old_total` should never be less than the new total because // `consolidate_unlocked` strictly subtracts balance. - if ledger.total < old_total { + if new_total < old_total { // Already checked that this won't overflow by entry condition. - let value = old_total - ledger.total; + let value = old_total - new_total; Self::deposit_event(Event::<T>::Withdrawn { stash, amount: value }); } @@ -163,10 +178,15 @@ impl<T: Config> Pallet<T> { .with_weight(T::WeightInfo::payout_stakers_alive_staked(0)) })?; - let controller = Self::bonded(&validator_stash).ok_or_else(|| { - Error::<T>::NotStash.with_weight(T::WeightInfo::payout_stakers_alive_staked(0)) + let account = StakingAccount::Stash(validator_stash.clone()); + let mut ledger = Self::ledger(account.clone()).or_else(|_| { + if StakingLedger::<T>::is_bonded(account) { + Err(Error::<T>::NotController.into()) + } else { + Err(Error::<T>::NotStash.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))) + } })?; - let mut ledger = <Ledger<T>>::get(&controller).ok_or(Error::<T>::NotController)?; + let stash = ledger.stash.clone(); ledger .claimed_rewards @@ -185,11 +205,11 @@ impl<T: Config> Pallet<T> { .defensive_map_err(|_| Error::<T>::BoundNotMet)?, } - let exposure = <ErasStakersClipped<T>>::get(&era, &ledger.stash); + let exposure = <ErasStakersClipped<T>>::get(&era, &stash); // Input data seems good, no errors allowed after this point - <Ledger<T>>::insert(&controller, &ledger); + ledger.update()?; // Get Era reward points. It has TOTAL and INDIVIDUAL // Find the fraction of the era reward that belongs to the validator @@ -200,11 +220,8 @@ impl<T: Config> Pallet<T> { let era_reward_points = <ErasRewardPoints<T>>::get(&era); let total_reward_points = era_reward_points.total; - let validator_reward_points = era_reward_points - .individual - .get(&ledger.stash) - .copied() - .unwrap_or_else(Zero::zero); + let validator_reward_points = + era_reward_points.individual.get(&stash).copied().unwrap_or_else(Zero::zero); // Nothing to do if they have no reward points. if validator_reward_points.is_zero() { @@ -231,19 +248,15 @@ impl<T: Config> Pallet<T> { Self::deposit_event(Event::<T>::PayoutStarted { era_index: era, - validator_stash: ledger.stash.clone(), + validator_stash: stash.clone(), }); let mut total_imbalance = PositiveImbalanceOf::<T>::zero(); // We can now make total validator payout: if let Some((imbalance, dest)) = - Self::make_payout(&ledger.stash, validator_staking_payout + validator_commission_payout) + Self::make_payout(&stash, validator_staking_payout + validator_commission_payout) { - Self::deposit_event(Event::<T>::Rewarded { - stash: ledger.stash, - dest, - amount: imbalance.peek(), - }); + Self::deposit_event(Event::<T>::Rewarded { stash, dest, amount: imbalance.peek() }); total_imbalance.subsume(imbalance); } @@ -278,14 +291,6 @@ impl<T: Config> Pallet<T> { Ok(Some(T::WeightInfo::payout_stakers_alive_staked(nominator_payout_count)).into()) } - /// Update the ledger for a controller. - /// - /// This will also update the stash lock. - pub(crate) fn update_ledger(controller: &T::AccountId, ledger: &StakingLedger<T>) { - T::Currency::set_lock(STAKING_ID, &ledger.stash, ledger.total, WithdrawReasons::all()); - <Ledger<T>>::insert(controller, ledger); - } - /// Chill a stash account. pub(crate) fn chill_stash(stash: &T::AccountId) { let chilled_as_validator = Self::do_remove_validator(stash); @@ -301,24 +306,30 @@ impl<T: Config> Pallet<T> { stash: &T::AccountId, amount: BalanceOf<T>, ) -> Option<(PositiveImbalanceOf<T>, RewardDestination<T::AccountId>)> { - let maybe_imbalance = match Self::payee(stash) { + let dest = Self::payee(StakingAccount::Stash(stash.clone())); + let maybe_imbalance = match dest { RewardDestination::Controller => Self::bonded(stash) .map(|controller| T::Currency::deposit_creating(&controller, amount)), RewardDestination::Stash => T::Currency::deposit_into_existing(stash, amount).ok(), - RewardDestination::Staked => Self::bonded(stash) - .and_then(|c| Self::ledger(&c).map(|l| (c, l))) - .and_then(|(controller, mut l)| { - l.active += amount; - l.total += amount; + RewardDestination::Staked => Self::ledger(Stash(stash.clone())) + .and_then(|mut ledger| { + ledger.active += amount; + ledger.total += amount; let r = T::Currency::deposit_into_existing(stash, amount).ok(); - Self::update_ledger(&controller, &l); - r - }), + + let _ = ledger + .update() + .defensive_proof("ledger fetched from storage, so it exists; qed."); + + Ok(r) + }) + .unwrap_or_default(), RewardDestination::Account(dest_account) => Some(T::Currency::deposit_creating(&dest_account, amount)), RewardDestination::None => None, }; - maybe_imbalance.map(|imbalance| (imbalance, Self::payee(stash))) + maybe_imbalance + .map(|imbalance| (imbalance, Self::payee(StakingAccount::Stash(stash.clone())))) } /// Plan a new session potentially trigger a new era. @@ -407,7 +418,6 @@ impl<T: Config> Pallet<T> { } /// Start a new era. It does: - /// /// * Increment `active_era.index`, /// * reset `active_era.start`, /// * update `BondedEras` and apply slashes. @@ -666,18 +676,16 @@ impl<T: Config> Pallet<T> { /// - after a `withdraw_unbonded()` call that frees all of a stash's bonded balance. /// - through `reap_stash()` if the balance has fallen to zero (through slashing). pub(crate) fn kill_stash(stash: &T::AccountId, num_slashing_spans: u32) -> DispatchResult { - let controller = <Bonded<T>>::get(stash).ok_or(Error::<T>::NotStash)?; - - slashing::clear_stash_metadata::<T>(stash, num_slashing_spans)?; + slashing::clear_stash_metadata::<T>(&stash, num_slashing_spans)?; - <Bonded<T>>::remove(stash); - <Ledger<T>>::remove(&controller); + // removes controller from `Bonded` and staking ledger from `Ledger`, as well as reward + // setting of the stash in `Payee`. + StakingLedger::<T>::kill(&stash)?; - <Payee<T>>::remove(stash); - Self::do_remove_validator(stash); - Self::do_remove_nominator(stash); + Self::do_remove_validator(&stash); + Self::do_remove_nominator(&stash); - frame_system::Pallet::<T>::dec_consumers(stash); + frame_system::Pallet::<T>::dec_consumers(&stash); Ok(()) } @@ -1123,13 +1131,7 @@ impl<T: Config> ElectionDataProvider for Pallet<T> { <Bonded<T>>::insert(voter.clone(), voter.clone()); <Ledger<T>>::insert( voter.clone(), - StakingLedger { - stash: voter.clone(), - active: stake, - total: stake, - unlocking: Default::default(), - claimed_rewards: Default::default(), - }, + StakingLedger::<T>::new(voter.clone(), stake, Default::default()), ); Self::do_add_nominator(&voter, Nominations { targets, submitted_in: 0, suppressed: false }); @@ -1141,13 +1143,7 @@ impl<T: Config> ElectionDataProvider for Pallet<T> { <Bonded<T>>::insert(target.clone(), target.clone()); <Ledger<T>>::insert( target.clone(), - StakingLedger { - stash: target.clone(), - active: stake, - total: stake, - unlocking: Default::default(), - claimed_rewards: Default::default(), - }, + StakingLedger::<T>::new(target.clone(), stake, Default::default()), ); Self::do_add_validator( &target, @@ -1182,13 +1178,7 @@ impl<T: Config> ElectionDataProvider for Pallet<T> { <Bonded<T>>::insert(v.clone(), v.clone()); <Ledger<T>>::insert( v.clone(), - StakingLedger { - stash: v.clone(), - active: stake, - total: stake, - unlocking: Default::default(), - claimed_rewards: Default::default(), - }, + StakingLedger::<T>::new(v.clone(), stake, Default::default()), ); Self::do_add_validator( &v, @@ -1203,13 +1193,7 @@ impl<T: Config> ElectionDataProvider for Pallet<T> { <Bonded<T>>::insert(v.clone(), v.clone()); <Ledger<T>>::insert( v.clone(), - StakingLedger { - stash: v.clone(), - active: stake, - total: stake, - unlocking: Default::default(), - claimed_rewards: Default::default(), - }, + StakingLedger::<T>::new(v.clone(), stake, Default::default()), ); Self::do_add_nominator( &v, @@ -1459,9 +1443,9 @@ impl<T: Config> ScoreProvider<T::AccountId> for Pallet<T> { // this will clearly results in an inconsistent state, but it should not matter for a // benchmark. let active: BalanceOf<T> = weight.try_into().map_err(|_| ()).unwrap(); - let mut ledger = match Self::ledger(who) { - None => StakingLedger::default_from(who.clone()), - Some(l) => l, + let mut ledger = match Self::ledger(StakingAccount::Stash(who.clone())) { + Ok(l) => l, + Err(_) => StakingLedger::default_from(who.clone()), }; ledger.active = active; @@ -1652,9 +1636,9 @@ impl<T: Config> StakingInterface for Pallet<T> { } fn stash_by_ctrl(controller: &Self::AccountId) -> Result<Self::AccountId, DispatchError> { - Self::ledger(controller) + Self::ledger(Controller(controller.clone())) .map(|l| l.stash) - .ok_or(Error::<T>::NotController.into()) + .map_err(|e| e.into()) } fn is_exposed_in_era(who: &Self::AccountId, era: &EraIndex) -> bool { @@ -1672,10 +1656,9 @@ impl<T: Config> StakingInterface for Pallet<T> { } fn stake(who: &Self::AccountId) -> Result<Stake<BalanceOf<T>>, DispatchError> { - Self::bonded(who) - .and_then(|c| Self::ledger(c)) + Self::ledger(Stash(who.clone())) .map(|l| Stake { total: l.total, active: l.active }) - .ok_or(Error::<T>::NotStash.into()) + .map_err(|e| e.into()) } fn bond_extra(who: &Self::AccountId, extra: Self::Balance) -> DispatchResult { @@ -1700,7 +1683,7 @@ impl<T: Config> StakingInterface for Pallet<T> { who: Self::AccountId, num_slashing_spans: u32, ) -> Result<bool, DispatchError> { - let ctrl = Self::bonded(who).ok_or(Error::<T>::NotStash)?; + let ctrl = Self::bonded(&who).ok_or(Error::<T>::NotStash)?; Self::withdraw_unbonded(RawOrigin::Signed(ctrl.clone()).into(), num_slashing_spans) .map(|_| !Ledger::<T>::contains_key(&ctrl)) .map_err(|with_post| with_post.error) @@ -1727,8 +1710,7 @@ impl<T: Config> StakingInterface for Pallet<T> { fn status( who: &Self::AccountId, ) -> Result<sp_staking::StakerStatus<Self::AccountId>, DispatchError> { - let is_bonded = Self::bonded(who).is_some(); - if !is_bonded { + if !StakingLedger::<T>::is_bonded(StakingAccount::Stash(who.clone())) { return Err(Error::<T>::NotStash.into()) } @@ -1882,7 +1864,8 @@ impl<T: Config> Pallet<T> { fn ensure_ledger_consistent(ctrl: T::AccountId) -> Result<(), TryRuntimeError> { // ensures ledger.total == ledger.active + sum(ledger.unlocking). - let ledger = Self::ledger(ctrl.clone()).ok_or("Not a controller.")?; + let ledger = Self::ledger(StakingAccount::Controller(ctrl.clone()))?; + let real_total: BalanceOf<T> = ledger.unlocking.iter().fold(ledger.active, |a, c| a + c.value); ensure!(real_total == ledger.total, "ledger.total corrupt"); diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 0bcf932d90b44..f084299be8e13 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -25,8 +25,7 @@ use frame_support::{ pallet_prelude::*, traits::{ Currency, Defensive, DefensiveResult, DefensiveSaturating, EnsureOrigin, - EstimateNextNewSession, Get, LockIdentifier, LockableCurrency, OnUnbalanced, TryCollect, - UnixTime, + EstimateNextNewSession, Get, LockableCurrency, OnUnbalanced, TryCollect, UnixTime, }, weights::Weight, BoundedVec, @@ -36,7 +35,10 @@ use sp_runtime::{ traits::{CheckedSub, SaturatedConversion, StaticLookup, Zero}, ArithmeticError, Perbill, Percent, }; -use sp_staking::{EraIndex, SessionIndex}; +use sp_staking::{ + EraIndex, SessionIndex, + StakingAccount::{self, Controller, Stash}, +}; use sp_std::prelude::*; mod impls; @@ -50,7 +52,6 @@ use crate::{ UnappliedSlash, UnlockChunk, ValidatorPrefs, }; -const STAKING_ID: LockIdentifier = *b"staking "; // The speculative number of spans are used as an input of the weight annotation of // [`Call::unbond`], as the post dipatch weight may depend on the number of slashing span on the // account which is not provided as an input. The value set should be conservative but sensible. @@ -295,7 +296,6 @@ pub mod pallet { /// /// TWOX-NOTE: SAFE since `AccountId` is a secure hash. #[pallet::storage] - #[pallet::getter(fn bonded)] pub type Bonded<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, T::AccountId>; /// The minimum active bond to become and maintain the role of a nominator. @@ -317,15 +317,16 @@ pub mod pallet { pub type MinCommission<T: Config> = StorageValue<_, Perbill, ValueQuery>; /// Map from all (unlocked) "controller" accounts to the info regarding the staking. + /// + /// Note: All the reads and mutations to this storage *MUST* be done through the methods exposed + /// by [`StakingLedger`] to ensure data and lock consistency. #[pallet::storage] - #[pallet::getter(fn ledger)] pub type Ledger<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, StakingLedger<T>>; /// Where the reward payment should be made. Keyed by stash. /// /// TWOX-NOTE: SAFE since `AccountId` is a secure hash. #[pallet::storage] - #[pallet::getter(fn payee)] pub type Payee<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, RewardDestination<T::AccountId>, ValueQuery>; @@ -841,16 +842,11 @@ pub mod pallet { payee: RewardDestination<T::AccountId>, ) -> DispatchResult { let stash = ensure_signed(origin)?; - let controller_to_be_deprecated = stash.clone(); - if <Bonded<T>>::contains_key(&stash) { + if StakingLedger::<T>::is_bonded(StakingAccount::Stash(stash.clone())) { return Err(Error::<T>::AlreadyBonded.into()) } - if <Ledger<T>>::contains_key(&controller_to_be_deprecated) { - return Err(Error::<T>::AlreadyPaired.into()) - } - // Reject a bond which is considered to be _dust_. if value < T::Currency::minimum_balance() { return Err(Error::<T>::InsufficientBond.into()) @@ -858,11 +854,6 @@ pub mod pallet { frame_system::Pallet::<T>::inc_consumers(&stash).map_err(|_| Error::<T>::BadState)?; - // You're auto-bonded forever, here. We might improve this by only bonding when - // you actually validate/nominate and remove once you unbond __everything__. - <Bonded<T>>::insert(&stash, &stash); - <Payee<T>>::insert(&stash, payee); - let current_era = CurrentEra::<T>::get().unwrap_or(0); let history_depth = T::HistoryDepth::get(); let last_reward_era = current_era.saturating_sub(history_depth); @@ -870,19 +861,21 @@ pub mod pallet { let stash_balance = T::Currency::free_balance(&stash); let value = value.min(stash_balance); Self::deposit_event(Event::<T>::Bonded { stash: stash.clone(), amount: value }); - let item = StakingLedger { - stash: stash.clone(), - total: value, - active: value, - unlocking: Default::default(), - claimed_rewards: (last_reward_era..current_era) + let ledger = StakingLedger::<T>::new( + stash.clone(), + value, + (last_reward_era..current_era) .try_collect() // Since last_reward_era is calculated as `current_era - // HistoryDepth`, following bound is always expected to be // satisfied. .defensive_map_err(|_| Error::<T>::BoundNotMet)?, - }; - Self::update_ledger(&controller_to_be_deprecated, &item); + ); + + // You're auto-bonded forever, here. We might improve this by only bonding when + // you actually validate/nominate and remove once you unbond __everything__. + ledger.bond(payee)?; + Ok(()) } @@ -908,8 +901,7 @@ pub mod pallet { ) -> DispatchResult { let stash = ensure_signed(origin)?; - let controller = Self::bonded(&stash).ok_or(Error::<T>::NotStash)?; - let mut ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?; + let mut ledger = Self::ledger(StakingAccount::Stash(stash.clone()))?; let stash_balance = T::Currency::free_balance(&stash); if let Some(extra) = stash_balance.checked_sub(&ledger.total) { @@ -923,11 +915,10 @@ pub mod pallet { ); // NOTE: ledger must be updated prior to calling `Self::weight_of`. - Self::update_ledger(&controller, &ledger); + ledger.update()?; // update this staker in the sorted list, if they exist in it. if T::VoterList::contains(&stash) { - let _ = - T::VoterList::on_update(&stash, Self::weight_of(&ledger.stash)).defensive(); + let _ = T::VoterList::on_update(&stash, Self::weight_of(&stash)).defensive(); } Self::deposit_event(Event::<T>::Bonded { stash, amount: extra }); @@ -963,9 +954,8 @@ pub mod pallet { #[pallet::compact] value: BalanceOf<T>, ) -> DispatchResultWithPostInfo { let controller = ensure_signed(origin)?; - let unlocking = Self::ledger(&controller) - .map(|l| l.unlocking.len()) - .ok_or(Error::<T>::NotController)?; + let unlocking = + Self::ledger(Controller(controller.clone())).map(|l| l.unlocking.len())?; // if there are no unlocking chunks available, try to withdraw chunks older than // `BondingDuration` to proceed with the unbonding. @@ -981,8 +971,9 @@ pub mod pallet { // we need to fetch the ledger again because it may have been mutated in the call // to `Self::do_withdraw_unbonded` above. - let mut ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?; + let mut ledger = Self::ledger(Controller(controller))?; let mut value = value.min(ledger.active); + let stash = ledger.stash.clone(); ensure!( ledger.unlocking.len() < T::MaxUnlockingChunks::get() as usize, @@ -998,9 +989,9 @@ pub mod pallet { ledger.active = Zero::zero(); } - let min_active_bond = if Nominators::<T>::contains_key(&ledger.stash) { + let min_active_bond = if Nominators::<T>::contains_key(&stash) { MinNominatorBond::<T>::get() - } else if Validators::<T>::contains_key(&ledger.stash) { + } else if Validators::<T>::contains_key(&stash) { MinValidatorBond::<T>::get() } else { Zero::zero() @@ -1024,15 +1015,14 @@ pub mod pallet { .map_err(|_| Error::<T>::NoMoreChunks)?; }; // NOTE: ledger must be updated prior to calling `Self::weight_of`. - Self::update_ledger(&controller, &ledger); + ledger.update()?; // update this staker in the sorted list, if they exist in it. - if T::VoterList::contains(&ledger.stash) { - let _ = T::VoterList::on_update(&ledger.stash, Self::weight_of(&ledger.stash)) - .defensive(); + if T::VoterList::contains(&stash) { + let _ = T::VoterList::on_update(&stash, Self::weight_of(&stash)).defensive(); } - Self::deposit_event(Event::<T>::Unbonded { stash: ledger.stash, amount: value }); + Self::deposit_event(Event::<T>::Unbonded { stash, amount: value }); } let actual_weight = if let Some(withdraw_weight) = maybe_withdraw_weight { @@ -1089,7 +1079,7 @@ pub mod pallet { pub fn validate(origin: OriginFor<T>, prefs: ValidatorPrefs) -> DispatchResult { let controller = ensure_signed(origin)?; - let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?; + let ledger = Self::ledger(Controller(controller))?; ensure!(ledger.active >= MinValidatorBond::<T>::get(), Error::<T>::InsufficientBond); let stash = &ledger.stash; @@ -1135,7 +1125,8 @@ pub mod pallet { ) -> DispatchResult { let controller = ensure_signed(origin)?; - let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?; + let ledger = Self::ledger(StakingAccount::Controller(controller.clone()))?; + ensure!(ledger.active >= MinNominatorBond::<T>::get(), Error::<T>::InsufficientBond); let stash = &ledger.stash; @@ -1202,7 +1193,9 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::chill())] pub fn chill(origin: OriginFor<T>) -> DispatchResult { let controller = ensure_signed(origin)?; - let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?; + + let ledger = Self::ledger(StakingAccount::Controller(controller))?; + Self::chill_stash(&ledger.stash); Ok(()) } @@ -1226,9 +1219,11 @@ pub mod pallet { payee: RewardDestination<T::AccountId>, ) -> DispatchResult { let controller = ensure_signed(origin)?; - let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?; - let stash = &ledger.stash; - <Payee<T>>::insert(stash, payee); + let ledger = Self::ledger(Controller(controller))?; + let _ = ledger + .set_payee(payee) + .defensive_proof("ledger was retrieved from storage, thus its bonded; qed."); + Ok(()) } @@ -1250,18 +1245,24 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::set_controller())] pub fn set_controller(origin: OriginFor<T>) -> DispatchResult { let stash = ensure_signed(origin)?; - let old_controller = Self::bonded(&stash).ok_or(Error::<T>::NotStash)?; - if <Ledger<T>>::contains_key(&stash) { - return Err(Error::<T>::AlreadyPaired.into()) - } - if old_controller != stash { - <Bonded<T>>::insert(&stash, &stash); - if let Some(l) = <Ledger<T>>::take(&old_controller) { - <Ledger<T>>::insert(&stash, l); + // the bonded map and ledger are mutated directly as this extrinsic is related to a + // (temporary) passive migration. + Self::ledger(StakingAccount::Stash(stash.clone())).map(|ledger| { + let controller = ledger.controller() + .defensive_proof("ledger was fetched used the StakingInterface, so controller field must exist; qed.") + .ok_or(Error::<T>::NotController)?; + + if controller == stash { + // stash is already its own controller. + return Err(Error::<T>::AlreadyPaired.into()) } - } - Ok(()) + // update bond and ledger. + <Ledger<T>>::remove(controller); + <Bonded<T>>::insert(&stash, &stash); + <Ledger<T>>::insert(&stash, ledger); + Ok(()) + })? } /// Sets the ideal number of validators. @@ -1409,11 +1410,9 @@ pub mod pallet { ) -> DispatchResult { ensure_root(origin)?; - // Remove all staking-related information. + // Remove all staking-related information and lock. Self::kill_stash(&stash, num_slashing_spans)?; - // Remove the lock. - T::Currency::remove_lock(STAKING_ID, &stash); Ok(()) } @@ -1502,7 +1501,7 @@ pub mod pallet { #[pallet::compact] value: BalanceOf<T>, ) -> DispatchResultWithPostInfo { let controller = ensure_signed(origin)?; - let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?; + let ledger = Self::ledger(Controller(controller))?; ensure!(!ledger.unlocking.is_empty(), Error::<T>::NoUnlockChunk); let initial_unlocking = ledger.unlocking.len() as u32; @@ -1515,16 +1514,18 @@ pub mod pallet { amount: rebonded_value, }); + let stash = ledger.stash.clone(); + let final_unlocking = ledger.unlocking.len(); + // NOTE: ledger must be updated prior to calling `Self::weight_of`. - Self::update_ledger(&controller, &ledger); - if T::VoterList::contains(&ledger.stash) { - let _ = T::VoterList::on_update(&ledger.stash, Self::weight_of(&ledger.stash)) - .defensive(); + ledger.update()?; + if T::VoterList::contains(&stash) { + let _ = T::VoterList::on_update(&stash, Self::weight_of(&stash)).defensive(); } let removed_chunks = 1u32 // for the case where the last iterated chunk is not removed .saturating_add(initial_unlocking) - .saturating_sub(ledger.unlocking.len() as u32); + .saturating_sub(final_unlocking as u32); Ok(Some(T::WeightInfo::rebond(removed_chunks)).into()) } @@ -1556,13 +1557,11 @@ pub mod pallet { let ed = T::Currency::minimum_balance(); let reapable = T::Currency::total_balance(&stash) < ed || - Self::ledger(Self::bonded(stash.clone()).ok_or(Error::<T>::NotStash)?) - .map(|l| l.total) - .unwrap_or_default() < ed; + Self::ledger(Stash(stash.clone())).map(|l| l.total).unwrap_or_default() < ed; ensure!(reapable, Error::<T>::FundedTarget); + // Remove all staking-related information and lock. Self::kill_stash(&stash, num_slashing_spans)?; - T::Currency::remove_lock(STAKING_ID, &stash); Ok(Pays::No.into()) } @@ -1582,7 +1581,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::kick(who.len() as u32))] pub fn kick(origin: OriginFor<T>, who: Vec<AccountIdLookupOf<T>>) -> DispatchResult { let controller = ensure_signed(origin)?; - let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?; + let ledger = Self::ledger(Controller(controller))?; let stash = &ledger.stash; for nom_stash in who @@ -1691,7 +1690,7 @@ pub mod pallet { pub fn chill_other(origin: OriginFor<T>, controller: T::AccountId) -> DispatchResult { // Anyone can call this function. let caller = ensure_signed(origin)?; - let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?; + let ledger = Self::ledger(Controller(controller.clone()))?; let stash = ledger.stash; // In order for one user to chill another user, the following conditions must be met: diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index bb02da73f6e5d..0d84d503733e6 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -597,15 +597,11 @@ pub fn do_slash<T: Config>( slashed_imbalance: &mut NegativeImbalanceOf<T>, slash_era: EraIndex, ) { - let controller = match <Pallet<T>>::bonded(stash).defensive() { - None => return, - Some(c) => c, - }; - - let mut ledger = match <Pallet<T>>::ledger(&controller) { - Some(ledger) => ledger, - None => return, // nothing to do. - }; + let mut ledger = + match Pallet::<T>::ledger(sp_staking::StakingAccount::Stash(stash.clone())).defensive() { + Ok(ledger) => ledger, + Err(_) => return, // nothing to do. + }; let value = ledger.slash(value, T::Currency::minimum_balance(), slash_era); @@ -618,7 +614,9 @@ pub fn do_slash<T: Config>( *reward_payout = reward_payout.saturating_sub(missing); } - <Pallet<T>>::update_ledger(&controller, &ledger); + let _ = ledger + .update() + .defensive_proof("ledger fetched from storage so it exists in storage; qed."); // trigger the event <Pallet<T>>::deposit_event(super::Event::<T>::Slashed { diff --git a/substrate/frame/staking/src/tests.rs b/substrate/frame/staking/src/tests.rs index 78183cfde9296..cb620f89f12c0 100644 --- a/substrate/frame/staking/src/tests.rs +++ b/substrate/frame/staking/src/tests.rs @@ -18,6 +18,7 @@ //! Tests for the module. use super::{ConfigOp, Event, *}; +use crate::ledger::StakingLedgerInspect; use frame_election_provider_support::{ bounds::{DataProviderBounds, ElectionBoundsBuilder}, ElectionProvider, SortedListProvider, Support, @@ -33,7 +34,7 @@ use pallet_balances::Error as BalancesError; use sp_runtime::{ assert_eq_error_rate, bounded_vec, traits::{BadOrigin, Dispatchable}, - Perbill, Percent, Rounding, TokenError, + Perbill, Percent, Perquintill, Rounding, TokenError, }; use sp_staking::{ offence::{DisableStrategy, OffenceDetails, OnOffenceHandler}, @@ -151,8 +152,8 @@ fn basic_setup_works() { // Account 11 controls its own stash, which is 100 * balance_factor units assert_eq!( - Staking::ledger(&11).unwrap(), - StakingLedger { + Ledger::get(&11).unwrap(), + StakingLedgerInspect::<Test> { stash: 11, total: 1000, active: 1000, @@ -162,17 +163,17 @@ fn basic_setup_works() { ); // Account 21 controls its own stash, which is 200 * balance_factor units assert_eq!( - Staking::ledger(&21), - Some(StakingLedger { + Ledger::get(&21).unwrap(), + StakingLedgerInspect::<Test> { stash: 21, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); // Account 1 does not control any stash - assert_eq!(Staking::ledger(&1), None); + assert!(Staking::ledger(1.into()).is_err()); // ValidatorPrefs are default assert_eq_uvec!( @@ -185,14 +186,14 @@ fn basic_setup_works() { ); assert_eq!( - Staking::ledger(101), - Some(StakingLedger { + Staking::ledger(101.into()).unwrap(), + StakingLedgerInspect { stash: 101, total: 500, active: 500, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]); @@ -265,7 +266,7 @@ fn change_controller_works() { #[test] fn change_controller_already_paired_once_stash() { ExtBuilder::default().build_and_execute(|| { - // 10 and 11 are bonded as controller and stash respectively. + // 11 and 11 are bonded as controller and stash respectively. assert_eq!(Staking::bonded(&11), Some(11)); // 11 is initially a validator. @@ -460,14 +461,14 @@ fn staking_should_work() { // Note: the stashed value of 4 is still lock assert_eq!( - Staking::ledger(&3), - Some(StakingLedger { + Staking::ledger(3.into()).unwrap(), + StakingLedgerInspect { stash: 3, total: 1500, active: 1500, unlocking: Default::default(), claimed_rewards: bounded_vec![0], - }) + } ); // e.g. it cannot reserve more than 500 that it has free from the total 2000 assert_noop!(Balances::reserve(&3, 501), BalancesError::<Test, _>::LiquidityRestrictions); @@ -717,9 +718,9 @@ fn nominators_also_get_slashed_pro_rata() { assert_eq!(initial_exposure.others.first().unwrap().who, 101); // staked values; - let nominator_stake = Staking::ledger(101).unwrap().active; + let nominator_stake = Staking::ledger(101.into()).unwrap().active; let nominator_balance = balances(&101).0; - let validator_stake = Staking::ledger(11).unwrap().active; + let validator_stake = Staking::ledger(11.into()).unwrap().active; let validator_balance = balances(&11).0; let exposed_stake = initial_exposure.total; let exposed_validator = initial_exposure.own; @@ -732,8 +733,8 @@ fn nominators_also_get_slashed_pro_rata() { ); // both stakes must have been decreased. - assert!(Staking::ledger(101).unwrap().active < nominator_stake); - assert!(Staking::ledger(11).unwrap().active < validator_stake); + assert!(Staking::ledger(101.into()).unwrap().active < nominator_stake); + assert!(Staking::ledger(11.into()).unwrap().active < validator_stake); let slash_amount = slash_percent * exposed_stake; let validator_share = @@ -746,8 +747,8 @@ fn nominators_also_get_slashed_pro_rata() { assert!(nominator_share > 0); // both stakes must have been decreased pro-rata. - assert_eq!(Staking::ledger(101).unwrap().active, nominator_stake - nominator_share); - assert_eq!(Staking::ledger(11).unwrap().active, validator_stake - validator_share); + assert_eq!(Staking::ledger(101.into()).unwrap().active, nominator_stake - nominator_share); + assert_eq!(Staking::ledger(11.into()).unwrap().active, validator_stake - validator_share); assert_eq!( balances(&101).0, // free balance nominator_balance - nominator_share, @@ -1047,14 +1048,14 @@ fn reward_destination_works() { assert_eq!(Balances::free_balance(11), 1000); // Check how much is at stake assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); // Compute total payout now for whole duration as other parameter won't change @@ -1065,19 +1066,19 @@ fn reward_destination_works() { mock::make_all_reward_payment(0); // Check that RewardDestination is Staked (default) - assert_eq!(Staking::payee(&11), RewardDestination::Staked); + assert_eq!(Staking::payee(11.into()), RewardDestination::Staked); // Check that reward went to the stash account of validator assert_eq!(Balances::free_balance(11), 1000 + total_payout_0); // Check that amount at stake increased accordingly assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000 + total_payout_0, active: 1000 + total_payout_0, unlocking: Default::default(), claimed_rewards: bounded_vec![0], - }) + } ); // Change RewardDestination to Stash @@ -1091,19 +1092,19 @@ fn reward_destination_works() { mock::make_all_reward_payment(1); // Check that RewardDestination is Stash - assert_eq!(Staking::payee(&11), RewardDestination::Stash); + assert_eq!(Staking::payee(11.into()), RewardDestination::Stash); // Check that reward went to the stash account assert_eq!(Balances::free_balance(11), 1000 + total_payout_0 + total_payout_1); // Check that amount at stake is NOT increased assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000 + total_payout_0, active: 1000 + total_payout_0, unlocking: Default::default(), claimed_rewards: bounded_vec![0, 1], - }) + } ); // Change RewardDestination to Controller @@ -1120,19 +1121,19 @@ fn reward_destination_works() { mock::make_all_reward_payment(2); // Check that RewardDestination is Controller - assert_eq!(Staking::payee(&11), RewardDestination::Controller); + assert_eq!(Staking::payee(11.into()), RewardDestination::Controller); // Check that reward went to the controller account assert_eq!(Balances::free_balance(11), 23150 + total_payout_2); // Check that amount at stake is NOT increased assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000 + total_payout_0, active: 1000 + total_payout_0, unlocking: Default::default(), claimed_rewards: bounded_vec![0, 1, 2], - }) + } ); }); } @@ -1185,14 +1186,14 @@ fn bond_extra_works() { assert_eq!(Staking::bonded(&11), Some(11)); // Check how much is at stake assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); // Give account 11 some large free balance greater than total @@ -1202,28 +1203,28 @@ fn bond_extra_works() { assert_ok!(Staking::bond_extra(RuntimeOrigin::signed(11), 100)); // There should be 100 more `total` and `active` in the ledger assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000 + 100, active: 1000 + 100, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); // Call the bond_extra function with a large number, should handle it assert_ok!(Staking::bond_extra(RuntimeOrigin::signed(11), Balance::max_value())); // The full amount of the funds should now be in the total and active assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000000, active: 1000000, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); }); } @@ -1254,14 +1255,14 @@ fn bond_extra_and_withdraw_unbonded_works() { // Initial state of 11 assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); assert_eq!( Staking::eras_stakers(active_era(), 11), @@ -1272,14 +1273,14 @@ fn bond_extra_and_withdraw_unbonded_works() { Staking::bond_extra(RuntimeOrigin::signed(11), 100).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000 + 100, active: 1000 + 100, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); // Exposure is a snapshot! only updated after the next era update. assert_ne!( @@ -1293,14 +1294,14 @@ fn bond_extra_and_withdraw_unbonded_works() { // ledger should be the same. assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000 + 100, active: 1000 + 100, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); // Exposure is now updated. assert_eq!( @@ -1311,27 +1312,27 @@ fn bond_extra_and_withdraw_unbonded_works() { // Unbond almost all of the funds in stash. Staking::unbond(RuntimeOrigin::signed(11), 1000).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000 + 100, active: 100, unlocking: bounded_vec![UnlockChunk { value: 1000, era: 2 + 3 }], claimed_rewards: bounded_vec![], - }), + }, ); // Attempting to free the balances now will fail. 2 eras need to pass. assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(11), 0)); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000 + 100, active: 100, unlocking: bounded_vec![UnlockChunk { value: 1000, era: 2 + 3 }], claimed_rewards: bounded_vec![], - }), + }, ); // trigger next era. @@ -1340,14 +1341,14 @@ fn bond_extra_and_withdraw_unbonded_works() { // nothing yet assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(11), 0)); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000 + 100, active: 100, unlocking: bounded_vec![UnlockChunk { value: 1000, era: 2 + 3 }], claimed_rewards: bounded_vec![], - }), + }, ); // trigger next era. @@ -1356,14 +1357,14 @@ fn bond_extra_and_withdraw_unbonded_works() { assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(11), 0)); // Now the value is free and the staking ledger is updated. assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 100, active: 100, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }), + }, ); }) } @@ -1390,7 +1391,7 @@ fn many_unbond_calls_should_work() { // `BondingDuration` == 3). assert_ok!(Staking::unbond(RuntimeOrigin::signed(11), 1)); assert_eq!( - Staking::ledger(&11).map(|l| l.unlocking.len()).unwrap(), + Staking::ledger(11.into()).map(|l| l.unlocking.len()).unwrap(), <<Test as Config>::MaxUnlockingChunks as Get<u32>>::get() as usize ); @@ -1405,7 +1406,7 @@ fn many_unbond_calls_should_work() { // only slots within last `BondingDuration` are filled. assert_eq!( - Staking::ledger(&11).map(|l| l.unlocking.len()).unwrap(), + Staking::ledger(11.into()).map(|l| l.unlocking.len()).unwrap(), <<Test as Config>::BondingDuration>::get() as usize ); }) @@ -1459,14 +1460,14 @@ fn rebond_works() { // Initial state of 11 assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); mock::start_active_era(2); @@ -1478,66 +1479,66 @@ fn rebond_works() { // Unbond almost all of the funds in stash. Staking::unbond(RuntimeOrigin::signed(11), 900).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 100, unlocking: bounded_vec![UnlockChunk { value: 900, era: 2 + 3 }], claimed_rewards: bounded_vec![], - }) + } ); // Re-bond all the funds unbonded. Staking::rebond(RuntimeOrigin::signed(11), 900).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); // Unbond almost all of the funds in stash. Staking::unbond(RuntimeOrigin::signed(11), 900).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 100, unlocking: bounded_vec![UnlockChunk { value: 900, era: 5 }], claimed_rewards: bounded_vec![], - }) + } ); // Re-bond part of the funds unbonded. Staking::rebond(RuntimeOrigin::signed(11), 500).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 600, unlocking: bounded_vec![UnlockChunk { value: 400, era: 5 }], claimed_rewards: bounded_vec![], - }) + } ); // Re-bond the remainder of the funds unbonded. Staking::rebond(RuntimeOrigin::signed(11), 500).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); // Unbond parts of the funds in stash. @@ -1545,27 +1546,27 @@ fn rebond_works() { Staking::unbond(RuntimeOrigin::signed(11), 300).unwrap(); Staking::unbond(RuntimeOrigin::signed(11), 300).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 100, unlocking: bounded_vec![UnlockChunk { value: 900, era: 5 }], claimed_rewards: bounded_vec![], - }) + } ); // Re-bond part of the funds unbonded. Staking::rebond(RuntimeOrigin::signed(11), 500).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 600, unlocking: bounded_vec![UnlockChunk { value: 400, era: 5 }], claimed_rewards: bounded_vec![], - }) + } ); }) } @@ -1585,14 +1586,14 @@ fn rebond_is_fifo() { // Initial state of 10 assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); mock::start_active_era(2); @@ -1600,14 +1601,14 @@ fn rebond_is_fifo() { // Unbond some of the funds in stash. Staking::unbond(RuntimeOrigin::signed(11), 400).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 600, unlocking: bounded_vec![UnlockChunk { value: 400, era: 2 + 3 }], claimed_rewards: bounded_vec![], - }) + } ); mock::start_active_era(3); @@ -1615,8 +1616,8 @@ fn rebond_is_fifo() { // Unbond more of the funds in stash. Staking::unbond(RuntimeOrigin::signed(11), 300).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 300, @@ -1625,7 +1626,7 @@ fn rebond_is_fifo() { UnlockChunk { value: 300, era: 3 + 3 }, ], claimed_rewards: bounded_vec![], - }) + } ); mock::start_active_era(4); @@ -1633,8 +1634,8 @@ fn rebond_is_fifo() { // Unbond yet more of the funds in stash. Staking::unbond(RuntimeOrigin::signed(11), 200).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 100, @@ -1644,14 +1645,14 @@ fn rebond_is_fifo() { UnlockChunk { value: 200, era: 4 + 3 }, ], claimed_rewards: bounded_vec![], - }) + } ); // Re-bond half of the unbonding funds. Staking::rebond(RuntimeOrigin::signed(11), 400).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 500, @@ -1660,7 +1661,7 @@ fn rebond_is_fifo() { UnlockChunk { value: 100, era: 3 + 3 }, ], claimed_rewards: bounded_vec![], - }) + } ); }) } @@ -1682,27 +1683,27 @@ fn rebond_emits_right_value_in_event() { // Unbond almost all of the funds in stash. Staking::unbond(RuntimeOrigin::signed(11), 900).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 100, unlocking: bounded_vec![UnlockChunk { value: 900, era: 1 + 3 }], claimed_rewards: bounded_vec![], - }) + } ); // Re-bond less than the total Staking::rebond(RuntimeOrigin::signed(11), 100).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 200, unlocking: bounded_vec![UnlockChunk { value: 800, era: 1 + 3 }], claimed_rewards: bounded_vec![], - }) + } ); // Event emitted should be correct assert_eq!(*staking_events().last().unwrap(), Event::Bonded { stash: 11, amount: 100 }); @@ -1710,14 +1711,14 @@ fn rebond_emits_right_value_in_event() { // Re-bond way more than available Staking::rebond(RuntimeOrigin::signed(11), 100_000).unwrap(); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); // Event emitted should be correct, only 800 assert_eq!(*staking_events().last().unwrap(), Event::Bonded { stash: 11, amount: 800 }); @@ -1747,7 +1748,7 @@ fn reward_to_stake_works() { ErasStakers::<Test>::insert(0, 21, Exposure { total: 69, own: 69, others: vec![] }); <Ledger<Test>>::insert( &20, - StakingLedger { + StakingLedgerInspect { stash: 21, total: 69, active: 69, @@ -1806,16 +1807,7 @@ fn reap_stash_works() { // no easy way to cause an account to go below ED, we tweak their staking ledger // instead. - Ledger::<Test>::insert( - 11, - StakingLedger { - stash: 11, - total: 5, - active: 5, - unlocking: Default::default(), - claimed_rewards: bounded_vec![], - }, - ); + Ledger::<Test>::insert(11, StakingLedger::<Test>::new(11, 5, bounded_vec![])); // reap-able assert_ok!(Staking::reap_stash(RuntimeOrigin::signed(20), 11, 0)); @@ -1937,14 +1929,14 @@ fn bond_with_no_staked_value() { // unbonding even 1 will cause all to be unbonded. assert_ok!(Staking::unbond(RuntimeOrigin::signed(1), 1)); assert_eq!( - Staking::ledger(1), - Some(StakingLedger { + Staking::ledger(1.into()).unwrap(), + StakingLedgerInspect { stash: 1, active: 0, total: 5, unlocking: bounded_vec![UnlockChunk { value: 5, era: 3 }], claimed_rewards: bounded_vec![], - }) + } ); mock::start_active_era(1); @@ -1952,14 +1944,14 @@ fn bond_with_no_staked_value() { // not yet removed. assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(1), 0)); - assert!(Staking::ledger(1).is_some()); + assert!(Staking::ledger(1.into()).is_ok()); assert_eq!(Balances::locks(&1)[0].amount, 5); mock::start_active_era(3); // poof. Account 1 is removed from the staking system. assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(1), 0)); - assert!(Staking::ledger(1).is_none()); + assert!(Staking::ledger(1.into()).is_err()); assert_eq!(Balances::locks(&1).len(), 0); }); } @@ -2044,7 +2036,7 @@ fn bond_with_duplicate_vote_should_be_ignored_by_election_provider() { // ensure all have equal stake. assert_eq!( <Validators<Test>>::iter() - .map(|(v, _)| (v, Staking::ledger(v).unwrap().total)) + .map(|(v, _)| (v, Staking::ledger(v.into()).unwrap().total)) .collect::<Vec<_>>(), vec![(31, 1000), (21, 1000), (11, 1000)], ); @@ -2096,7 +2088,7 @@ fn bond_with_duplicate_vote_should_be_ignored_by_election_provider_elected() { // ensure all have equal stake. assert_eq!( <Validators<Test>>::iter() - .map(|(v, _)| (v, Staking::ledger(v).unwrap().total)) + .map(|(v, _)| (v, Staking::ledger(v.into()).unwrap().total)) .collect::<Vec<_>>(), vec![(31, 1000), (21, 1000), (11, 1000)], ); @@ -2982,7 +2974,7 @@ fn retroactive_deferred_slashes_one_before() { mock::start_active_era(4); - assert_eq!(Staking::ledger(11).unwrap().total, 1000); + assert_eq!(Staking::ledger(11.into()).unwrap().total, 1000); // slash happens after the next line. mock::start_active_era(5); @@ -2997,9 +2989,9 @@ fn retroactive_deferred_slashes_one_before() { )); // their ledger has already been slashed. - assert_eq!(Staking::ledger(11).unwrap().total, 900); + assert_eq!(Staking::ledger(11.into()).unwrap().total, 900); assert_ok!(Staking::unbond(RuntimeOrigin::signed(11), 1000)); - assert_eq!(Staking::ledger(11).unwrap().total, 900); + assert_eq!(Staking::ledger(11.into()).unwrap().total, 900); }) } @@ -3032,7 +3024,7 @@ fn staker_cannot_bail_deferred_slash() { assert_eq!( Ledger::<Test>::get(101).unwrap(), - StakingLedger { + StakingLedgerInspect { active: 0, total: 500, stash: 101, @@ -3782,14 +3774,14 @@ fn test_payout_stakers() { // We track rewards in `claimed_rewards` vec assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: bounded_vec![1] - }) + } ); for i in 3..16 { @@ -3813,14 +3805,14 @@ fn test_payout_stakers() { // We track rewards in `claimed_rewards` vec assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: (1..=14).collect::<Vec<_>>().try_into().unwrap() - }) + } ); let last_era = 99; @@ -3846,14 +3838,14 @@ fn test_payout_stakers() { expected_last_reward_era )); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: bounded_vec![expected_start_reward_era, expected_last_reward_era] - }) + } ); // Out of order claims works. @@ -3861,8 +3853,8 @@ fn test_payout_stakers() { assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 23)); assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 42)); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 1000, @@ -3874,7 +3866,7 @@ fn test_payout_stakers() { 69, expected_last_reward_era ] - }) + } ); }); } @@ -4073,26 +4065,26 @@ fn bond_during_era_correctly_populates_claimed_rewards() { // Era = None bond_validator(9, 1000); assert_eq!( - Staking::ledger(&9), - Some(StakingLedger { + Staking::ledger(9.into()).unwrap(), + StakingLedgerInspect { stash: 9, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: bounded_vec![], - }) + } ); mock::start_active_era(5); bond_validator(11, 1000); assert_eq!( - Staking::ledger(&11), - Some(StakingLedger { + Staking::ledger(11.into()).unwrap(), + StakingLedgerInspect { stash: 11, total: 1000, active: 1000, unlocking: Default::default(), claimed_rewards: (0..5).collect::<Vec<_>>().try_into().unwrap(), - }) + } ); // make sure only era upto history depth is stored @@ -4101,8 +4093,8 @@ fn bond_during_era_correctly_populates_claimed_rewards() { mock::start_active_era(current_era); bond_validator(13, 1000); assert_eq!( - Staking::ledger(&13), - Some(StakingLedger { + Staking::ledger(13.into()).unwrap(), + StakingLedgerInspect { stash: 13, total: 1000, active: 1000, @@ -4111,7 +4103,7 @@ fn bond_during_era_correctly_populates_claimed_rewards() { .collect::<Vec<_>>() .try_into() .unwrap(), - }) + } ); }); } @@ -4200,6 +4192,7 @@ fn payout_creates_controller() { false, ) .unwrap(); + assert_ok!(Staking::nominate(RuntimeOrigin::signed(controller), vec![11])); // kill controller @@ -4356,8 +4349,8 @@ fn cannot_rebond_to_lower_than_ed() { .build_and_execute(|| { // initial stuff. assert_eq!( - Staking::ledger(&21).unwrap(), - StakingLedger { + Staking::ledger(21.into()).unwrap(), + StakingLedgerInspect { stash: 21, total: 11 * 1000, active: 11 * 1000, @@ -4370,8 +4363,8 @@ fn cannot_rebond_to_lower_than_ed() { assert_ok!(Staking::chill(RuntimeOrigin::signed(21))); assert_ok!(Staking::unbond(RuntimeOrigin::signed(21), 11 * 1000)); assert_eq!( - Staking::ledger(&21).unwrap(), - StakingLedger { + Staking::ledger(21.into()).unwrap(), + StakingLedgerInspect { stash: 21, total: 11 * 1000, active: 0, @@ -4396,8 +4389,8 @@ fn cannot_bond_extra_to_lower_than_ed() { .build_and_execute(|| { // initial stuff. assert_eq!( - Staking::ledger(&21).unwrap(), - StakingLedger { + Staking::ledger(21.into()).unwrap(), + StakingLedgerInspect { stash: 21, total: 11 * 1000, active: 11 * 1000, @@ -4410,8 +4403,8 @@ fn cannot_bond_extra_to_lower_than_ed() { assert_ok!(Staking::chill(RuntimeOrigin::signed(21))); assert_ok!(Staking::unbond(RuntimeOrigin::signed(21), 11 * 1000)); assert_eq!( - Staking::ledger(&21).unwrap(), - StakingLedger { + Staking::ledger(21.into()).unwrap(), + StakingLedgerInspect { stash: 21, total: 11 * 1000, active: 0, @@ -4437,8 +4430,8 @@ fn do_not_die_when_active_is_ed() { .build_and_execute(|| { // given assert_eq!( - Staking::ledger(&21).unwrap(), - StakingLedger { + Staking::ledger(21.into()).unwrap(), + StakingLedgerInspect { stash: 21, total: 1000 * ed, active: 1000 * ed, @@ -4454,8 +4447,8 @@ fn do_not_die_when_active_is_ed() { // then assert_eq!( - Staking::ledger(&21).unwrap(), - StakingLedger { + Staking::ledger(21.into()).unwrap(), + StakingLedgerInspect { stash: 21, total: ed, active: ed, @@ -5530,15 +5523,14 @@ fn force_apply_min_commission_works() { #[test] fn proportional_slash_stop_slashing_if_remaining_zero() { let c = |era, value| UnlockChunk::<Balance> { era, value }; + + // we have some chunks, but they are not affected. + let unlocking = bounded_vec![c(1, 10), c(2, 10)]; + // Given - let mut ledger = StakingLedger::<Test> { - stash: 123, - total: 40, - active: 20, - // we have some chunks, but they are not affected. - unlocking: bounded_vec![c(1, 10), c(2, 10)], - claimed_rewards: bounded_vec![], - }; + let mut ledger = StakingLedger::<Test>::new(123, 20, bounded_vec![]); + ledger.total = 40; + ledger.unlocking = unlocking; assert_eq!(BondingDuration::get(), 3); @@ -5550,13 +5542,7 @@ fn proportional_slash_stop_slashing_if_remaining_zero() { fn proportional_ledger_slash_works() { let c = |era, value| UnlockChunk::<Balance> { era, value }; // Given - let mut ledger = StakingLedger::<Test> { - stash: 123, - total: 10, - active: 10, - unlocking: bounded_vec![], - claimed_rewards: bounded_vec![], - }; + let mut ledger = StakingLedger::<Test>::new(123, 10, bounded_vec![]); assert_eq!(BondingDuration::get(), 3); // When we slash a ledger with no unlocking chunks @@ -5795,8 +5781,8 @@ fn pre_bonding_era_cannot_be_claimed() { let claimed_rewards: BoundedVec<_, _> = (start_reward_era..=last_reward_era).collect::<Vec<_>>().try_into().unwrap(); assert_eq!( - Staking::ledger(&3).unwrap(), - StakingLedger { + Staking::ledger(3.into()).unwrap(), + StakingLedgerInspect { stash: 3, total: 1500, active: 1500, @@ -5823,7 +5809,7 @@ fn pre_bonding_era_cannot_be_claimed() { // decoding will fail now since Staking Ledger is in corrupt state HistoryDepth::set(history_depth - 1); - assert_eq!(Staking::ledger(&4), None); + assert!(Staking::ledger(4.into()).is_err()); // make sure stakers still cannot claim rewards that they are not meant to assert_noop!( @@ -5861,8 +5847,8 @@ fn reducing_history_depth_abrupt() { let claimed_rewards: BoundedVec<_, _> = (start_reward_era..=last_reward_era).collect::<Vec<_>>().try_into().unwrap(); assert_eq!( - Staking::ledger(&3).unwrap(), - StakingLedger { + Staking::ledger(3.into()).unwrap(), + StakingLedgerInspect { stash: 3, total: 1500, active: 1500, @@ -5900,8 +5886,8 @@ fn reducing_history_depth_abrupt() { let claimed_rewards: BoundedVec<_, _> = (start_reward_era..=last_reward_era).collect::<Vec<_>>().try_into().unwrap(); assert_eq!( - Staking::ledger(&5).unwrap(), - StakingLedger { + Staking::ledger(5.into()).unwrap(), + StakingLedgerInspect { stash: 5, total: 1200, active: 1200, @@ -5924,7 +5910,7 @@ fn reducing_max_unlocking_chunks_abrupt() { MaxUnlockingChunks::set(2); start_active_era(10); assert_ok!(Staking::bond(RuntimeOrigin::signed(3), 300, RewardDestination::Staked)); - assert!(matches!(Staking::ledger(3), Some(_))); + assert!(matches!(Staking::ledger(3.into()), Ok(_))); // when staker unbonds assert_ok!(Staking::unbond(RuntimeOrigin::signed(3), 20)); @@ -5933,8 +5919,8 @@ fn reducing_max_unlocking_chunks_abrupt() { // => 10 + 3 = 13 let expected_unlocking: BoundedVec<UnlockChunk<Balance>, MaxUnlockingChunks> = bounded_vec![UnlockChunk { value: 20 as Balance, era: 13 as EraIndex }]; - assert!(matches!(Staking::ledger(3), - Some(StakingLedger { + assert!(matches!(Staking::ledger(3.into()), + Ok(StakingLedger { unlocking, .. }) if unlocking==expected_unlocking)); @@ -5945,8 +5931,8 @@ fn reducing_max_unlocking_chunks_abrupt() { // then another unlock chunk is added let expected_unlocking: BoundedVec<UnlockChunk<Balance>, MaxUnlockingChunks> = bounded_vec![UnlockChunk { value: 20, era: 13 }, UnlockChunk { value: 50, era: 14 }]; - assert!(matches!(Staking::ledger(3), - Some(StakingLedger { + assert!(matches!(Staking::ledger(3.into()), + Ok(StakingLedger { unlocking, .. }) if unlocking==expected_unlocking)); @@ -6134,3 +6120,123 @@ mod staking_interface { }) } } + +mod ledger { + use super::*; + + #[test] + fn paired_account_works() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(Staking::bond( + RuntimeOrigin::signed(10), + 100, + RewardDestination::Controller + )); + + assert_eq!(<Bonded<Test>>::get(&10), Some(10)); + assert_eq!( + StakingLedger::<Test>::paired_account(StakingAccount::Controller(10)), + Some(10) + ); + assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Stash(10)), Some(10)); + + assert_eq!(<Bonded<Test>>::get(&42), None); + assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Controller(42)), None); + assert_eq!(StakingLedger::<Test>::paired_account(StakingAccount::Stash(42)), None); + + // bond manually stash with different controller. This is deprecated but the migration + // has not been complete yet (controller: 100, stash: 200) + assert_ok!(bond_controller_stash(100, 200)); + assert_eq!(<Bonded<Test>>::get(&200), Some(100)); + assert_eq!( + StakingLedger::<Test>::paired_account(StakingAccount::Controller(100)), + Some(200) + ); + assert_eq!( + StakingLedger::<Test>::paired_account(StakingAccount::Stash(200)), + Some(100) + ); + }) + } + + #[test] + fn get_ledger_works() { + ExtBuilder::default().build_and_execute(|| { + // stash does not exist + assert!(StakingLedger::<Test>::get(StakingAccount::Stash(42)).is_err()); + + // bonded and paired + assert_eq!(<Bonded<Test>>::get(&11), Some(11)); + + match StakingLedger::<Test>::get(StakingAccount::Stash(11)) { + Ok(ledger) => { + assert_eq!(ledger.controller(), Some(11)); + assert_eq!(ledger.stash, 11); + }, + Err(_) => panic!("staking ledger must exist"), + }; + + // bond manually stash with different controller. This is deprecated but the migration + // has not been complete yet (controller: 100, stash: 200) + assert_ok!(bond_controller_stash(100, 200)); + assert_eq!(<Bonded<Test>>::get(&200), Some(100)); + + match StakingLedger::<Test>::get(StakingAccount::Stash(200)) { + Ok(ledger) => { + assert_eq!(ledger.controller(), Some(100)); + assert_eq!(ledger.stash, 200); + }, + Err(_) => panic!("staking ledger must exist"), + }; + + match StakingLedger::<Test>::get(StakingAccount::Controller(100)) { + Ok(ledger) => { + assert_eq!(ledger.controller(), Some(100)); + assert_eq!(ledger.stash, 200); + }, + Err(_) => panic!("staking ledger must exist"), + }; + }) + } + + #[test] + fn bond_works() { + ExtBuilder::default().build_and_execute(|| { + assert!(!StakingLedger::<Test>::is_bonded(StakingAccount::Stash(42))); + assert!(<Bonded<Test>>::get(&42).is_none()); + + let mut ledger: StakingLedger<Test> = StakingLedger::default_from(42); + let reward_dest = RewardDestination::Account(10); + + assert_ok!(ledger.clone().bond(reward_dest)); + assert!(StakingLedger::<Test>::is_bonded(StakingAccount::Stash(42))); + assert!(<Bonded<Test>>::get(&42).is_some()); + assert_eq!(<Payee<Test>>::get(&42), reward_dest); + + // cannot bond again. + assert!(ledger.clone().bond(reward_dest).is_err()); + + // once bonded, update works as expected. + ledger.claimed_rewards = bounded_vec![1]; + assert_ok!(ledger.update()); + }) + } + + #[test] + fn is_bonded_works() { + ExtBuilder::default().build_and_execute(|| { + assert!(!StakingLedger::<Test>::is_bonded(StakingAccount::Stash(42))); + assert!(!StakingLedger::<Test>::is_bonded(StakingAccount::Controller(42))); + + // adds entry to Bonded without Ledger pair (should not happen). + <Bonded<Test>>::insert(42, 42); + assert!(!StakingLedger::<Test>::is_bonded(StakingAccount::Controller(42))); + + assert_eq!(<Bonded<Test>>::get(&11), Some(11)); + assert!(StakingLedger::<Test>::is_bonded(StakingAccount::Stash(11))); + assert!(StakingLedger::<Test>::is_bonded(StakingAccount::Controller(11))); + + <Bonded<Test>>::remove(42); // ensures try-state checks pass. + }) + } +} diff --git a/substrate/primitives/staking/src/lib.rs b/substrate/primitives/staking/src/lib.rs index 8b5797d7918fb..dfc18987d1525 100644 --- a/substrate/primitives/staking/src/lib.rs +++ b/substrate/primitives/staking/src/lib.rs @@ -37,6 +37,23 @@ pub type SessionIndex = u32; /// Counter for the number of eras that have passed. pub type EraIndex = u32; +/// Representation of a staking account, which may be a stash or controller account. +/// +/// Note: once the controller is completely deprecated, this enum can also be deprecated in favor of +/// the stash account. Tracking issue: <https://github.com/paritytech/substrate/issues/6927>. +#[derive(Clone, Debug)] +pub enum StakingAccount<AccountId> { + Stash(AccountId), + Controller(AccountId), +} + +#[cfg(feature = "std")] +impl<AccountId> From<AccountId> for StakingAccount<AccountId> { + fn from(account: AccountId) -> Self { + StakingAccount::Stash(account) + } +} + /// Representation of the status of a staker. #[derive(RuntimeDebug, TypeInfo)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone))] From 91c4360c3cd4a2bba756e9cbc19f08377bea6d6e Mon Sep 17 00:00:00 2001 From: Daan van der Plas <93204684+Daanvdplas@users.noreply.github.com> Date: Sun, 15 Oct 2023 23:32:25 +0200 Subject: [PATCH 50/54] fix: GoAhead signal only set when runtime upgrade is enacted from parachain side (#1176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runtime code of a parachain can be replaced on the relay-chain via: [cumulus]: [enact_authorized_upgrade](https://github.com/paritytech/polkadot-sdk/blob/1a38d6d6be42b30e8be3ffccec75a4ec995fef9d/cumulus/pallets/parachain-system/src/lib.rs#L661); this is used for a runtime upgrade when a parachain is not bricked. [polkadot] (these are used when a parachain is bricked): - [force_set_current_code](https://github.com/paritytech/polkadot-sdk/blob/1a38d6d6be42b30e8be3ffccec75a4ec995fef9d/polkadot/runtime/parachains/src/paras/mod.rs#L823): immediately changes the runtime code of a given para without a pvf check (root). - [force_schedule_code_upgrade](https://github.com/paritytech/polkadot-sdk/blob/1a38d6d6be42b30e8be3ffccec75a4ec995fef9d/polkadot/runtime/parachains/src/paras/mod.rs#L864): schedules a change to the runtime code of a given para including a pvf check of the new code (root). - [schedule_code_upgrade](https://github.com/paritytech/polkadot-sdk/blob/1a38d6d6be42b30e8be3ffccec75a4ec995fef9d/polkadot/runtime/common/src/paras_registrar.rs#L395): schedules a change to the runtime code of a given para including a pvf check of the new code. Besides root, the parachain or parachain manager can call this extrinsic given that the parachain is unlocked. Polkadot signals a parachain to be ready for a runtime upgrade through the [GoAhead](https://github.com/paritytech/polkadot-sdk/blob/e49493442a9377be9344c06a4990e17423783d41/polkadot/primitives/src/v5/mod.rs#L1229) signal. When in cumulus `enact_authorized_upgrade` is executed, the same underlying helper function of `force_schedule_code_upgrade` & `schedule_code_upgrade`: [schedule_code_upgrade](https://github.com/paritytech/polkadot/blob/09b61286da11921a3dda0a8e4015ceb9ef9cffca/runtime/parachains/src/paras/mod.rs#L1778), is called on the relay-chain, which sets the `GoAhead` signal (if the pvf is accepted). If Cumulus receives the `GoAhead` signal from polkadot without having the `PendingValidationCode` ready, it will panic ([ref](https://github.com/paritytech/polkadot/pull/7412)). For `enact_authorized_upgrade` we know for sure the `PendingValidationCode` is set. On the contrary, for `force_schedule_code_upgrade` & `schedule_code_upgrade` this is not the case. This PR includes a flag such that the `GoAhead` signal will only be set when a runtime upgrade is enacted by the parachain (`enact_authorized_upgrade`). additional info: https://github.com/paritytech/polkadot/pull/7412 Closes #641 --------- Co-authored-by: Bastian Köcher <git@kchr.de> Co-authored-by: Bastian Köcher <info@kchr.de> --- .../runtime/common/src/paras_registrar/mod.rs | 4 +- .../runtime/parachains/src/inclusion/mod.rs | 9 +- .../runtime/parachains/src/inclusion/tests.rs | 10 +- polkadot/runtime/parachains/src/lib.rs | 5 +- .../parachains/src/paras/benchmarking.rs | 8 +- .../src/paras/benchmarking/pvf_check.rs | 1 + polkadot/runtime/parachains/src/paras/mod.rs | 50 +++- .../runtime/parachains/src/paras/tests.rs | 227 +++++++++++++++++- 8 files changed, 280 insertions(+), 34 deletions(-) diff --git a/polkadot/runtime/common/src/paras_registrar/mod.rs b/polkadot/runtime/common/src/paras_registrar/mod.rs index f2751803a4133..8b8c6d89d0185 100644 --- a/polkadot/runtime/common/src/paras_registrar/mod.rs +++ b/polkadot/runtime/common/src/paras_registrar/mod.rs @@ -29,7 +29,7 @@ use frame_system::{self, ensure_root, ensure_signed}; use primitives::{HeadData, Id as ParaId, ValidationCode, LOWEST_PUBLIC_ID}; use runtime_parachains::{ configuration, ensure_parachain, - paras::{self, ParaGenesisArgs}, + paras::{self, ParaGenesisArgs, SetGoAhead}, Origin, ParaLifecycle, }; use sp_std::{prelude::*, result}; @@ -412,7 +412,7 @@ pub mod pallet { new_code: ValidationCode, ) -> DispatchResult { Self::ensure_root_para_or_owner(origin, para)?; - runtime_parachains::schedule_code_upgrade::<T>(para, new_code)?; + runtime_parachains::schedule_code_upgrade::<T>(para, new_code, SetGoAhead::No)?; Ok(()) } diff --git a/polkadot/runtime/parachains/src/inclusion/mod.rs b/polkadot/runtime/parachains/src/inclusion/mod.rs index bb16c804150dc..e34286d750ac1 100644 --- a/polkadot/runtime/parachains/src/inclusion/mod.rs +++ b/polkadot/runtime/parachains/src/inclusion/mod.rs @@ -21,7 +21,8 @@ use crate::{ configuration::{self, HostConfiguration}, - disputes, dmp, hrmp, paras, + disputes, dmp, hrmp, + paras::{self, SetGoAhead}, scheduler::{self, AvailabilityTimeoutStatus}, shared::{self, AllowedRelayParentsTracker}, }; @@ -448,8 +449,9 @@ impl fmt::Debug for UmpAcceptanceCheckErr { "the ump queue would have grown past the max size permitted by config ({} > {})", total_size, limit, ), - UmpAcceptanceCheckErr::IsOffboarding => - write!(fmt, "upward message rejected because the para is off-boarding",), + UmpAcceptanceCheckErr::IsOffboarding => { + write!(fmt, "upward message rejected because the para is off-boarding") + }, } } } @@ -885,6 +887,7 @@ impl<T: Config> Pallet<T> { new_code, now, &config, + SetGoAhead::Yes, )); } diff --git a/polkadot/runtime/parachains/src/inclusion/tests.rs b/polkadot/runtime/parachains/src/inclusion/tests.rs index 7677108d73dec..6bb731671f6f8 100644 --- a/polkadot/runtime/parachains/src/inclusion/tests.rs +++ b/polkadot/runtime/parachains/src/inclusion/tests.rs @@ -1255,7 +1255,13 @@ fn candidate_checks() { let cfg = Configuration::config(); let expected_at = 10 + cfg.validation_upgrade_delay; assert_eq!(expected_at, 12); - Paras::schedule_code_upgrade(chain_a, vec![1, 2, 3, 4].into(), expected_at, &cfg); + Paras::schedule_code_upgrade( + chain_a, + vec![1, 2, 3, 4].into(), + expected_at, + &cfg, + SetGoAhead::Yes, + ); } assert_noop!( @@ -2235,7 +2241,7 @@ fn para_upgrade_delay_scheduled_from_inclusion() { let cause = &active_vote_state.causes()[0]; // Upgrade block is the block of inclusion, not candidate's parent. assert_matches!(cause, - paras::PvfCheckCause::Upgrade { id, included_at } + paras::PvfCheckCause::Upgrade { id, included_at, set_go_ahead: SetGoAhead::Yes } if id == &chain_a && included_at == &7 ); }); diff --git a/polkadot/runtime/parachains/src/lib.rs b/polkadot/runtime/parachains/src/lib.rs index 64365f17b7e8b..e0ace86d3794a 100644 --- a/polkadot/runtime/parachains/src/lib.rs +++ b/polkadot/runtime/parachains/src/lib.rs @@ -53,7 +53,7 @@ mod mock; mod ump_tests; pub use origin::{ensure_parachain, Origin}; -pub use paras::ParaLifecycle; +pub use paras::{ParaLifecycle, SetGoAhead}; use primitives::{HeadData, Id as ParaId, ValidationCode}; use sp_runtime::{DispatchResult, FixedU128}; @@ -89,8 +89,9 @@ pub fn schedule_parachain_downgrade<T: paras::Config>(id: ParaId) -> Result<(), pub fn schedule_code_upgrade<T: paras::Config>( id: ParaId, new_code: ValidationCode, + set_go_ahead: SetGoAhead, ) -> DispatchResult { - paras::Pallet::<T>::schedule_code_upgrade_external(id, new_code) + paras::Pallet::<T>::schedule_code_upgrade_external(id, new_code, set_go_ahead) } /// Sets the current parachain head with the given id. diff --git a/polkadot/runtime/parachains/src/paras/benchmarking.rs b/polkadot/runtime/parachains/src/paras/benchmarking.rs index 5c060547601f5..554f0c15af24f 100644 --- a/polkadot/runtime/parachains/src/paras/benchmarking.rs +++ b/polkadot/runtime/parachains/src/paras/benchmarking.rs @@ -124,7 +124,13 @@ benchmarks! { let expired = frame_system::Pallet::<T>::block_number().saturating_sub(One::one()); let config = HostConfiguration::<BlockNumberFor<T>>::default(); generate_disordered_pruning::<T>(); - Pallet::<T>::schedule_code_upgrade(para_id, ValidationCode(vec![0]), expired, &config); + Pallet::<T>::schedule_code_upgrade( + para_id, + ValidationCode(vec![0]), + expired, + &config, + SetGoAhead::Yes, + ); }: _(RawOrigin::Root, para_id, new_head) verify { assert_last_event::<T>(Event::NewHeadNoted(para_id).into()); diff --git a/polkadot/runtime/parachains/src/paras/benchmarking/pvf_check.rs b/polkadot/runtime/parachains/src/paras/benchmarking/pvf_check.rs index ab5e13124436d..05c4c9c37b4d9 100644 --- a/polkadot/runtime/parachains/src/paras/benchmarking/pvf_check.rs +++ b/polkadot/runtime/parachains/src/paras/benchmarking/pvf_check.rs @@ -177,6 +177,7 @@ where validation_code, /* relay_parent_number */ 1u32.into(), &configuration::Pallet::<T>::config(), + SetGoAhead::Yes, ); } else { let r = Pallet::<T>::schedule_para_initialize( diff --git a/polkadot/runtime/parachains/src/paras/mod.rs b/polkadot/runtime/parachains/src/paras/mod.rs index 2f370b5bfe472..cd73d23bdadb3 100644 --- a/polkadot/runtime/parachains/src/paras/mod.rs +++ b/polkadot/runtime/parachains/src/paras/mod.rs @@ -386,9 +386,18 @@ pub(crate) enum PvfCheckCause<BlockNumber> { /// /// See https://github.com/paritytech/polkadot/issues/4601 for detailed explanation. included_at: BlockNumber, + /// Whether or not the given para should be sent the `GoAhead` signal. + set_go_ahead: SetGoAhead, }, } +/// Should the `GoAhead` signal be set after a successful check of the new wasm binary? +#[derive(Debug, Copy, Clone, PartialEq, TypeInfo, Decode, Encode)] +pub enum SetGoAhead { + Yes, + No, +} + impl<BlockNumber> PvfCheckCause<BlockNumber> { /// Returns the ID of the para that initiated or subscribed to the pre-checking vote. fn para_id(&self) -> ParaId { @@ -888,7 +897,13 @@ pub mod pallet { ) -> DispatchResult { ensure_root(origin)?; let config = configuration::Pallet::<T>::config(); - Self::schedule_code_upgrade(para, new_code, relay_parent_number, &config); + Self::schedule_code_upgrade( + para, + new_code, + relay_parent_number, + &config, + SetGoAhead::No, + ); Self::deposit_event(Event::CodeUpgradeScheduled(para)); Ok(()) } @@ -1186,6 +1201,7 @@ impl<T: Config> Pallet<T> { pub(crate) fn schedule_code_upgrade_external( id: ParaId, new_code: ValidationCode, + set_go_ahead: SetGoAhead, ) -> DispatchResult { // Check that we can schedule an upgrade at all. ensure!(Self::can_upgrade_validation_code(id), Error::<T>::CannotUpgradeCode); @@ -1193,7 +1209,7 @@ impl<T: Config> Pallet<T> { let current_block = frame_system::Pallet::<T>::block_number(); // Schedule the upgrade with a delay just like if a parachain triggered the upgrade. let upgrade_block = current_block.saturating_add(config.validation_upgrade_delay); - Self::schedule_code_upgrade(id, new_code, upgrade_block, &config); + Self::schedule_code_upgrade(id, new_code, upgrade_block, &config, set_go_ahead); Self::deposit_event(Event::CodeUpgradeScheduled(id)); Ok(()) } @@ -1534,8 +1550,15 @@ impl<T: Config> Pallet<T> { PvfCheckCause::Onboarding(id) => { weight += Self::proceed_with_onboarding(*id, sessions_observed); }, - PvfCheckCause::Upgrade { id, included_at } => { - weight += Self::proceed_with_upgrade(*id, code_hash, now, *included_at, cfg); + PvfCheckCause::Upgrade { id, included_at, set_go_ahead } => { + weight += Self::proceed_with_upgrade( + *id, + code_hash, + now, + *included_at, + cfg, + *set_go_ahead, + ); }, } } @@ -1568,6 +1591,7 @@ impl<T: Config> Pallet<T> { now: BlockNumberFor<T>, relay_parent_number: BlockNumberFor<T>, cfg: &configuration::HostConfiguration<BlockNumberFor<T>>, + set_go_ahead: SetGoAhead, ) -> Weight { let mut weight = Weight::zero(); @@ -1591,12 +1615,15 @@ impl<T: Config> Pallet<T> { weight += T::DbWeight::get().reads_writes(1, 4); FutureCodeUpgrades::<T>::insert(&id, expected_at); - UpcomingUpgrades::<T>::mutate(|upcoming_upgrades| { - let insert_idx = upcoming_upgrades - .binary_search_by_key(&expected_at, |&(_, b)| b) - .unwrap_or_else(|idx| idx); - upcoming_upgrades.insert(insert_idx, (id, expected_at)); - }); + // Only set an upcoming upgrade if `GoAhead` signal should be set for the respective para. + if set_go_ahead == SetGoAhead::Yes { + UpcomingUpgrades::<T>::mutate(|upcoming_upgrades| { + let insert_idx = upcoming_upgrades + .binary_search_by_key(&expected_at, |&(_, b)| b) + .unwrap_or_else(|idx| idx); + upcoming_upgrades.insert(insert_idx, (id, expected_at)); + }); + } let expected_at = expected_at.saturated_into(); let log = ConsensusLog::ParaScheduleUpgradeCode(id, *code_hash, expected_at); @@ -1835,6 +1862,7 @@ impl<T: Config> Pallet<T> { new_code: ValidationCode, inclusion_block_number: BlockNumberFor<T>, cfg: &configuration::HostConfiguration<BlockNumberFor<T>>, + set_go_ahead: SetGoAhead, ) -> Weight { let mut weight = T::DbWeight::get().reads(1); @@ -1884,7 +1912,7 @@ impl<T: Config> Pallet<T> { }); weight += Self::kick_off_pvf_check( - PvfCheckCause::Upgrade { id, included_at: inclusion_block_number }, + PvfCheckCause::Upgrade { id, included_at: inclusion_block_number, set_go_ahead }, code_hash, new_code, cfg, diff --git a/polkadot/runtime/parachains/src/paras/tests.rs b/polkadot/runtime/parachains/src/paras/tests.rs index c511c65d47330..cca200c2765e3 100644 --- a/polkadot/runtime/parachains/src/paras/tests.rs +++ b/polkadot/runtime/parachains/src/paras/tests.rs @@ -436,7 +436,13 @@ fn code_upgrade_applied_after_delay() { // this parablock is in the context of block 1. let expected_at = 1 + validation_upgrade_delay; let next_possible_upgrade_at = 1 + validation_upgrade_cooldown; - Paras::schedule_code_upgrade(para_id, new_code.clone(), 1, &Configuration::config()); + Paras::schedule_code_upgrade( + para_id, + new_code.clone(), + 1, + &Configuration::config(), + SetGoAhead::Yes, + ); // Include votes for super-majority. submit_super_majority_pvf_votes(&new_code, EXPECTED_SESSION, true); @@ -504,6 +510,127 @@ fn code_upgrade_applied_after_delay() { }); } +#[test] +fn code_upgrade_applied_without_setting_go_ahead_signal() { + let code_retention_period = 10; + let validation_upgrade_delay = 5; + let validation_upgrade_cooldown = 10; + + let original_code = ValidationCode(vec![1, 2, 3]); + let paras = vec![( + 0u32.into(), + ParaGenesisArgs { + para_kind: ParaKind::Parachain, + genesis_head: dummy_head_data(), + validation_code: original_code.clone(), + }, + )]; + + let genesis_config = MockGenesisConfig { + paras: GenesisConfig { paras, ..Default::default() }, + configuration: crate::configuration::GenesisConfig { + config: HostConfiguration { + code_retention_period, + validation_upgrade_delay, + validation_upgrade_cooldown, + ..Default::default() + }, + }, + ..Default::default() + }; + + new_test_ext(genesis_config).execute_with(|| { + check_code_is_stored(&original_code); + + let para_id = ParaId::from(0); + let new_code = ValidationCode(vec![4, 5, 6]); + + // Wait for at least one session change to set active validators. + const EXPECTED_SESSION: SessionIndex = 1; + run_to_block(2, Some(vec![1])); + assert_eq!(Paras::current_code(¶_id), Some(original_code.clone())); + + let (expected_at, next_possible_upgrade_at) = { + // this parablock is in the context of block 1. + let expected_at = 1 + validation_upgrade_delay; + let next_possible_upgrade_at = 1 + validation_upgrade_cooldown; + // `set_go_ahead` parameter set to `false` which prevents signaling the parachain + // with the `GoAhead` signal. + Paras::schedule_code_upgrade( + para_id, + new_code.clone(), + 1, + &Configuration::config(), + SetGoAhead::No, + ); + // Include votes for super-majority. + submit_super_majority_pvf_votes(&new_code, EXPECTED_SESSION, true); + + Paras::note_new_head(para_id, Default::default(), 1); + + assert!(Paras::past_code_meta(¶_id).most_recent_change().is_none()); + assert_eq!(FutureCodeUpgrades::<Test>::get(¶_id), Some(expected_at)); + assert_eq!(FutureCodeHash::<Test>::get(¶_id), Some(new_code.hash())); + assert_eq!(UpcomingUpgrades::<Test>::get(), vec![]); + assert_eq!(UpgradeCooldowns::<Test>::get(), vec![(para_id, next_possible_upgrade_at)]); + assert_eq!(Paras::current_code(¶_id), Some(original_code.clone())); + check_code_is_stored(&original_code); + check_code_is_stored(&new_code); + + (expected_at, next_possible_upgrade_at) + }; + + run_to_block(expected_at, None); + + // the candidate is in the context of the parent of `expected_at`, + // thus does not trigger the code upgrade. However, now the `UpgradeGoAheadSignal` + // should not be set. + { + Paras::note_new_head(para_id, Default::default(), expected_at - 1); + + assert!(Paras::past_code_meta(¶_id).most_recent_change().is_none()); + assert_eq!(FutureCodeUpgrades::<Test>::get(¶_id), Some(expected_at)); + assert_eq!(FutureCodeHash::<Test>::get(¶_id), Some(new_code.hash())); + assert!(UpgradeGoAheadSignal::<Test>::get(¶_id).is_none()); + assert_eq!(Paras::current_code(¶_id), Some(original_code.clone())); + check_code_is_stored(&original_code); + check_code_is_stored(&new_code); + } + + run_to_block(expected_at + 1, None); + + // the candidate is in the context of `expected_at`, and triggers + // the upgrade. + { + Paras::note_new_head(para_id, Default::default(), expected_at); + + assert_eq!(Paras::past_code_meta(¶_id).most_recent_change(), Some(expected_at)); + assert_eq!( + PastCodeHash::<Test>::get(&(para_id, expected_at)), + Some(original_code.hash()), + ); + assert!(FutureCodeUpgrades::<Test>::get(¶_id).is_none()); + assert!(FutureCodeHash::<Test>::get(¶_id).is_none()); + assert!(UpgradeGoAheadSignal::<Test>::get(¶_id).is_none()); + assert_eq!(Paras::current_code(¶_id), Some(new_code.clone())); + assert_eq!( + UpgradeRestrictionSignal::<Test>::get(¶_id), + Some(UpgradeRestriction::Present), + ); + assert_eq!(UpgradeCooldowns::<Test>::get(), vec![(para_id, next_possible_upgrade_at)]); + check_code_is_stored(&original_code); + check_code_is_stored(&new_code); + } + + run_to_block(next_possible_upgrade_at + 1, None); + + { + assert!(UpgradeRestrictionSignal::<Test>::get(¶_id).is_none()); + assert!(UpgradeCooldowns::<Test>::get().is_empty()); + } + }); +} + #[test] fn code_upgrade_applied_after_delay_even_when_late() { let code_retention_period = 10; @@ -546,7 +673,13 @@ fn code_upgrade_applied_after_delay_even_when_late() { // this parablock is in the context of block 1. let expected_at = 1 + validation_upgrade_delay; let next_possible_upgrade_at = 1 + validation_upgrade_cooldown; - Paras::schedule_code_upgrade(para_id, new_code.clone(), 1, &Configuration::config()); + Paras::schedule_code_upgrade( + para_id, + new_code.clone(), + 1, + &Configuration::config(), + SetGoAhead::Yes, + ); // Include votes for super-majority. submit_super_majority_pvf_votes(&new_code, EXPECTED_SESSION, true); @@ -624,7 +757,13 @@ fn submit_code_change_when_not_allowed_is_err() { const EXPECTED_SESSION: SessionIndex = 1; run_to_block(1, Some(vec![1])); - Paras::schedule_code_upgrade(para_id, new_code.clone(), 1, &Configuration::config()); + Paras::schedule_code_upgrade( + para_id, + new_code.clone(), + 1, + &Configuration::config(), + SetGoAhead::Yes, + ); // Include votes for super-majority. submit_super_majority_pvf_votes(&new_code, EXPECTED_SESSION, true); @@ -636,7 +775,13 @@ fn submit_code_change_when_not_allowed_is_err() { // ignore it. Note that this is only true from perspective of this module. run_to_block(2, None); assert!(!Paras::can_upgrade_validation_code(para_id)); - Paras::schedule_code_upgrade(para_id, newer_code.clone(), 2, &Configuration::config()); + Paras::schedule_code_upgrade( + para_id, + newer_code.clone(), + 2, + &Configuration::config(), + SetGoAhead::Yes, + ); assert_eq!( FutureCodeUpgrades::<Test>::get(¶_id), Some(1 + validation_upgrade_delay), /* did not change since the same assertion from @@ -694,7 +839,13 @@ fn upgrade_restriction_elapsed_doesnt_mean_can_upgrade() { const EXPECTED_SESSION: SessionIndex = 1; run_to_block(1, Some(vec![1])); - Paras::schedule_code_upgrade(para_id, new_code.clone(), 0, &Configuration::config()); + Paras::schedule_code_upgrade( + para_id, + new_code.clone(), + 0, + &Configuration::config(), + SetGoAhead::Yes, + ); // Include votes for super-majority. submit_super_majority_pvf_votes(&new_code, EXPECTED_SESSION, true); @@ -713,7 +864,13 @@ fn upgrade_restriction_elapsed_doesnt_mean_can_upgrade() { assert!(!Paras::can_upgrade_validation_code(para_id)); // And scheduling another upgrade does not do anything. `expected_at` is still the same. - Paras::schedule_code_upgrade(para_id, newer_code.clone(), 30, &Configuration::config()); + Paras::schedule_code_upgrade( + para_id, + newer_code.clone(), + 30, + &Configuration::config(), + SetGoAhead::Yes, + ); assert_eq!(FutureCodeUpgrades::<Test>::get(¶_id), Some(0 + validation_upgrade_delay)); }); } @@ -765,7 +922,13 @@ fn full_parachain_cleanup_storage() { let expected_at = { // this parablock is in the context of block 1. let expected_at = 1 + validation_upgrade_delay; - Paras::schedule_code_upgrade(para_id, new_code.clone(), 1, &Configuration::config()); + Paras::schedule_code_upgrade( + para_id, + new_code.clone(), + 1, + &Configuration::config(), + SetGoAhead::Yes, + ); // Include votes for super-majority. submit_super_majority_pvf_votes(&new_code, EXPECTED_SESSION, true); @@ -860,6 +1023,7 @@ fn cannot_offboard_ongoing_pvf_check() { new_code.clone(), RELAY_PARENT, &Configuration::config(), + SetGoAhead::Yes, ); assert!(!Paras::pvfs_require_precheck().is_empty()); @@ -1012,7 +1176,13 @@ fn code_hash_at_returns_up_to_end_of_code_retention_period() { let para_id = ParaId::from(0); let old_code: ValidationCode = vec![1, 2, 3].into(); let new_code: ValidationCode = vec![4, 5, 6].into(); - Paras::schedule_code_upgrade(para_id, new_code.clone(), 0, &Configuration::config()); + Paras::schedule_code_upgrade( + para_id, + new_code.clone(), + 0, + &Configuration::config(), + SetGoAhead::Yes, + ); // Include votes for super-majority. submit_super_majority_pvf_votes(&new_code, EXPECTED_SESSION, true); @@ -1120,6 +1290,7 @@ fn pvf_check_coalescing_onboarding_and_upgrade() { validation_code.clone(), RELAY_PARENT, &Configuration::config(), + SetGoAhead::Yes, ); assert!(!Paras::pvfs_require_precheck().is_empty()); @@ -1224,7 +1395,13 @@ fn pvf_check_upgrade_reject() { // Expected current session index. const EXPECTED_SESSION: SessionIndex = 1; - Paras::schedule_code_upgrade(a, new_code.clone(), RELAY_PARENT, &Configuration::config()); + Paras::schedule_code_upgrade( + a, + new_code.clone(), + RELAY_PARENT, + &Configuration::config(), + SetGoAhead::Yes, + ); check_code_is_stored(&new_code); // 1/3 of validators vote against `new_code`. PVF should not be rejected yet. @@ -1404,7 +1581,13 @@ fn include_pvf_check_statement_refunds_weight() { // Expected current session index. const EXPECTED_SESSION: SessionIndex = 1; - Paras::schedule_code_upgrade(a, new_code.clone(), RELAY_PARENT, &Configuration::config()); + Paras::schedule_code_upgrade( + a, + new_code.clone(), + RELAY_PARENT, + &Configuration::config(), + SetGoAhead::Yes, + ); let mut stmts = IntoIterator::into_iter([0, 1, 2, 3]) .map(|i| { @@ -1499,7 +1682,13 @@ fn poke_unused_validation_code_doesnt_remove_code_with_users() { // Then we add a user to the code, say by upgrading. run_to_block(2, None); - Paras::schedule_code_upgrade(para_id, validation_code.clone(), 1, &Configuration::config()); + Paras::schedule_code_upgrade( + para_id, + validation_code.clone(), + 1, + &Configuration::config(), + SetGoAhead::Yes, + ); Paras::note_new_head(para_id, HeadData::default(), 1); // Finally we poke the code, which should not remove it from the storage. @@ -1564,7 +1753,13 @@ fn add_trusted_validation_code_insta_approval() { // Then some parachain upgrades it's code with the relay-parent 1. run_to_block(2, None); - Paras::schedule_code_upgrade(para_id, validation_code.clone(), 1, &Configuration::config()); + Paras::schedule_code_upgrade( + para_id, + validation_code.clone(), + 1, + &Configuration::config(), + SetGoAhead::Yes, + ); Paras::note_new_head(para_id, HeadData::default(), 1); // Verify that the code upgrade has `expected_at` set to `26`. @@ -1600,7 +1795,13 @@ fn add_trusted_validation_code_enacts_existing_pvf_vote() { new_test_ext(genesis_config).execute_with(|| { // First, some parachain upgrades it's code with the relay-parent 1. run_to_block(2, None); - Paras::schedule_code_upgrade(para_id, validation_code.clone(), 1, &Configuration::config()); + Paras::schedule_code_upgrade( + para_id, + validation_code.clone(), + 1, + &Configuration::config(), + SetGoAhead::Yes, + ); Paras::note_new_head(para_id, HeadData::default(), 1); // No upgrade should be scheduled at this point. PVF pre-checking vote should run for From 19f38ca3aa111f8f0cbddaab2d2e894f7c190c2b Mon Sep 17 00:00:00 2001 From: shuoer86 <129674997+shuoer86@users.noreply.github.com> Date: Mon, 16 Oct 2023 16:01:01 +0800 Subject: [PATCH 51/54] Fix typos (#1878) --- cumulus/client/consensus/common/src/tests.rs | 2 +- cumulus/client/relay-chain-interface/src/lib.rs | 6 +++--- .../relay-chain-rpc-interface/src/light_client_worker.rs | 2 +- .../src/node/disputes/dispute-coordinator.md | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cumulus/client/consensus/common/src/tests.rs b/cumulus/client/consensus/common/src/tests.rs index 22d3dd3abd49d..9658a0add790d 100644 --- a/cumulus/client/consensus/common/src/tests.rs +++ b/cumulus/client/consensus/common/src/tests.rs @@ -579,7 +579,7 @@ fn follow_new_best_sets_best_after_it_is_imported() { block_import_params.fork_choice = Some(ForkChoiceStrategy::Custom(false)); block_import_params.body = Some(body); - // Now import the unkown block to make it "known" + // Now import the unknown block to make it "known" client.import_block(block_import_params).await.unwrap(); loop { diff --git a/cumulus/client/relay-chain-interface/src/lib.rs b/cumulus/client/relay-chain-interface/src/lib.rs index a6970732447c3..3dda61635804d 100644 --- a/cumulus/client/relay-chain-interface/src/lib.rs +++ b/cumulus/client/relay-chain-interface/src/lib.rs @@ -41,7 +41,7 @@ pub type RelayChainResult<T> = Result<T, RelayChainError>; #[derive(thiserror::Error, Debug)] pub enum RelayChainError { - #[error("Error occured while calling relay chain runtime: {0}")] + #[error("Error occurred while calling relay chain runtime: {0}")] ApiError(#[from] ApiError), #[error("Timeout while waiting for relay-chain block `{0}` to be imported.")] WaitTimeout(PHash), @@ -53,7 +53,7 @@ pub enum RelayChainError { WaitBlockchainError(PHash, sp_blockchain::Error), #[error("Blockchain returned an error: {0}")] BlockchainError(#[from] sp_blockchain::Error), - #[error("State machine error occured: {0}")] + #[error("State machine error occurred: {0}")] StateMachineError(Box<dyn sp_state_machine::Error>), #[error("Unable to call RPC method '{0}'")] RpcCallError(String), @@ -67,7 +67,7 @@ pub enum RelayChainError { Application(#[from] Box<dyn std::error::Error + Send + Sync + 'static>), #[error("Prometheus error: {0}")] PrometheusError(#[from] PrometheusError), - #[error("Unspecified error occured: {0}")] + #[error("Unspecified error occurred: {0}")] GenericError(String), } diff --git a/cumulus/client/relay-chain-rpc-interface/src/light_client_worker.rs b/cumulus/client/relay-chain-rpc-interface/src/light_client_worker.rs index 84e66f9557109..6fd057e170b71 100644 --- a/cumulus/client/relay-chain-rpc-interface/src/light_client_worker.rs +++ b/cumulus/client/relay-chain-rpc-interface/src/light_client_worker.rs @@ -48,7 +48,7 @@ const MAX_SUBSCRIPTIONS: u32 = 64; #[derive(thiserror::Error, Debug)] enum LightClientError { - #[error("Error occured while executing smoldot request: {0}")] + #[error("Error occurred while executing smoldot request: {0}")] SmoldotError(String), #[error("Nothing returned from json_rpc_responses")] EmptyResult, diff --git a/polkadot/roadmap/implementers-guide/src/node/disputes/dispute-coordinator.md b/polkadot/roadmap/implementers-guide/src/node/disputes/dispute-coordinator.md index daba416e2639e..a9cb2741b0838 100644 --- a/polkadot/roadmap/implementers-guide/src/node/disputes/dispute-coordinator.md +++ b/polkadot/roadmap/implementers-guide/src/node/disputes/dispute-coordinator.md @@ -79,7 +79,7 @@ game, so we are not too woried about colluding approval voters getting away slas maintained anyway. There is however a separate problem, from colluding approval-voters, that is "lazy" approval voters. If it were easy and reliable for approval-voters to reconsider their vote, in case of an actual dispute, then they don't have a direct incentive (apart from playing a part in securing the network) to properly run the validation function at -all - they could just always vote "valid" totally risk free. (While they would alwasy risk a slash by voting invalid.) +all - they could just always vote "valid" totally risk free. (While they would always risk a slash by voting invalid.) So we do want to fetch approval votes from approval-voting. Importing votes is most efficient when batched. At the same @@ -125,7 +125,7 @@ moment the dispute concludes! Two concerns that come to mind, are easily address enough: We are worried about lazy approval checkers, the system does not need to be perfect. It should be enough if there is some risk of getting caught. 2. We are not worried about the dispute not concluding, as nodes will always send their own vote, regardless of it being - an explict or an already existing approval-vote. + an explicit or an already existing approval-vote. Conclusion: As long as we make sure, if our own approval vote gets imported (which would prevent dispute participation) to also distribute it via dispute-distribution, disputes can conclude. To mitigate raciness with approval-voting @@ -307,7 +307,7 @@ spam, then spam slots for the disputed candidate hash are cleared. This decremen which had voted invalid. To keep spam slots from filling up unnecessarily we want to clear spam slots whenever a candidate is seen to be backed -or included. Fortunately this behavior is acheived by clearing slots on vote import as described above. Because on chain +or included. Fortunately this behavior is achieved by clearing slots on vote import as described above. Because on chain backing votes are processed when a block backing the disputed candidate is discovered, spam slots are cleared for every backed candidate. Included candidates have also been seen as backed on the same fork, so decrementing spam slots is handled in that case as well. @@ -422,7 +422,7 @@ from them, so they would be an easy DoS target. In summary: The availability system was designed for raising disputes in a meaningful and secure way after availability was reached. Trying to raise disputes before does not meaningfully contribute to the systems security/might even weaken -it as attackers are warned before availability is reached, while at the same time adding signficant amount of +it as attackers are warned before availability is reached, while at the same time adding significant amount of complexity. We therefore punt on such disputes and concentrate on disputes the system was designed to handle. ### No Disputes for Already Finalized Blocks From 38ef04eb53d43acdf504eea86ee3b1c8e08115e8 Mon Sep 17 00:00:00 2001 From: Davide Galassi <davxy@datawok.net> Date: Mon, 16 Oct 2023 10:43:52 +0200 Subject: [PATCH 52/54] Arkworks Elliptic Curve utils overhaul (#1870) - Removal of Arkworks unit tests. These tests were just testing the arkworks upstream implementation which should be assumed correct. This is not the place to test well known dependencies. - Removal of some over-engineering. We just store the calls to Arkworks in one file. Per-curve sources are not required. - Docs formatting --- I also took the opportunity to bump the `bandersnatch-vrfs` crate revision internally providing some new shiny stuff. --- Cargo.lock | 174 +-------- substrate/primitives/core/Cargo.toml | 2 +- substrate/primitives/core/src/bandersnatch.rs | 33 +- .../primitives/crypto/ec-utils/Cargo.toml | 31 +- .../crypto/ec-utils/src/bls12_377.rs | 103 ------ .../crypto/ec-utils/src/bls12_381.rs | 219 ------------ .../primitives/crypto/ec-utils/src/bw6_761.rs | 103 ------ .../crypto/ec-utils/src/ed_on_bls12_377.rs | 56 --- .../src/ed_on_bls12_381_bandersnatch.rs | 94 ----- .../primitives/crypto/ec-utils/src/lib.rs | 337 ++++++++++-------- .../g1_compressed_valid_test_vectors.dat | Bin 48000 -> 0 bytes .../g1_uncompressed_valid_test_vectors.dat | Bin 96000 -> 0 bytes .../g2_compressed_valid_test_vectors.dat | Bin 96000 -> 0 bytes .../g2_uncompressed_valid_test_vectors.dat | Bin 192000 -> 0 bytes .../primitives/crypto/ec-utils/src/utils.rs | 39 +- 15 files changed, 218 insertions(+), 973 deletions(-) delete mode 100644 substrate/primitives/crypto/ec-utils/src/bls12_377.rs delete mode 100644 substrate/primitives/crypto/ec-utils/src/bls12_381.rs delete mode 100644 substrate/primitives/crypto/ec-utils/src/bw6_761.rs delete mode 100644 substrate/primitives/crypto/ec-utils/src/ed_on_bls12_377.rs delete mode 100644 substrate/primitives/crypto/ec-utils/src/ed_on_bls12_381_bandersnatch.rs delete mode 100644 substrate/primitives/crypto/ec-utils/src/test-data/g1_compressed_valid_test_vectors.dat delete mode 100644 substrate/primitives/crypto/ec-utils/src/test-data/g1_uncompressed_valid_test_vectors.dat delete mode 100644 substrate/primitives/crypto/ec-utils/src/test-data/g2_compressed_valid_test_vectors.dat delete mode 100644 substrate/primitives/crypto/ec-utils/src/test-data/g2_uncompressed_valid_test_vectors.dat diff --git a/Cargo.lock b/Cargo.lock index 855700f3c201a..2b2f09c19ec16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,26 +316,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" -[[package]] -name = "ark-algebra-test-templates" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "400bd3a79c741b1832f1416d4373ae077ef82ca14a8b4cee1248a2f11c8b9172" -dependencies = [ - "ark-ec", - "ark-ff", - "ark-serialize", - "ark-std", - "hex", - "num-bigint", - "num-integer", - "num-traits", - "serde", - "serde_derive", - "serde_json", - "sha2 0.10.7", -] - [[package]] name = "ark-bls12-377" version = "0.4.0" @@ -468,52 +448,24 @@ dependencies = [ "hashbrown 0.13.2", ] -[[package]] -name = "ark-r1cs-std" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de1d1472e5cb020cb3405ce2567c91c8d43f21b674aef37b0202f5c3304761db" -dependencies = [ - "ark-ec", - "ark-ff", - "ark-relations", - "ark-std", - "derivative", - "num-bigint", - "num-integer", - "num-traits", - "tracing", -] - -[[package]] -name = "ark-relations" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00796b6efc05a3f48225e59cb6a2cda78881e7c390872d5786aaf112f31fb4f0" -dependencies = [ - "ark-ff", - "ark-std", - "tracing", - "tracing-subscriber", -] - [[package]] name = "ark-scale" -version = "0.0.10" +version = "0.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b08346a3e38e2be792ef53ee168623c9244d968ff00cd70fb9932f6fe36393" +checksum = "51bd73bb6ddb72630987d37fa963e99196896c0d0ea81b7c894567e74a2f83af" dependencies = [ "ark-ec", "ark-ff", "ark-serialize", "ark-std", "parity-scale-codec", + "scale-info", ] [[package]] name = "ark-secret-scalar" version = "0.0.2" -source = "git+https://github.com/w3f/ring-vrf?rev=f4fe253#f4fe2534ccc6d916cd10d9c16891e673728ec8b4" +source = "git+https://github.com/w3f/ring-vrf?rev=4b09416#4b09416fd23383ec436ddac127d58c7b7cd392c6" dependencies = [ "ark-ec", "ark-ff", @@ -561,7 +513,7 @@ dependencies = [ [[package]] name = "ark-transcript" version = "0.0.2" -source = "git+https://github.com/w3f/ring-vrf?rev=f4fe253#f4fe2534ccc6d916cd10d9c16891e673728ec8b4" +source = "git+https://github.com/w3f/ring-vrf?rev=4b09416#4b09416fd23383ec436ddac127d58c7b7cd392c6" dependencies = [ "ark-ff", "ark-serialize", @@ -1195,7 +1147,7 @@ dependencies = [ [[package]] name = "bandersnatch_vrfs" version = "0.0.1" -source = "git+https://github.com/w3f/ring-vrf?rev=f4fe253#f4fe2534ccc6d916cd10d9c16891e673728ec8b4" +source = "git+https://github.com/w3f/ring-vrf?rev=4b09416#4b09416fd23383ec436ddac127d58c7b7cd392c6" dependencies = [ "ark-bls12-381", "ark-ec", @@ -2683,7 +2635,7 @@ dependencies = [ [[package]] name = "common" version = "0.1.0" -source = "git+https://github.com/w3f/ring-proof?rev=8657210#86572101f4210647984ab4efedba6b3fcc890895" +source = "git+https://github.com/w3f/ring-proof#edd1e90b847e560bf60fc2e8712235ccfa11a9a9" dependencies = [ "ark-ec", "ark-ff", @@ -4415,7 +4367,7 @@ checksum = "86e3bdc80eee6e16b2b6b0f87fbc98c04bee3455e35174c0de1a125d0688c632" [[package]] name = "dleq_vrf" version = "0.0.2" -source = "git+https://github.com/w3f/ring-vrf?rev=f4fe253#f4fe2534ccc6d916cd10d9c16891e673728ec8b4" +source = "git+https://github.com/w3f/ring-vrf?rev=4b09416#4b09416fd23383ec436ddac127d58c7b7cd392c6" dependencies = [ "ark-ec", "ark-ff", @@ -5730,10 +5682,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] @@ -13923,7 +13873,7 @@ dependencies = [ [[package]] name = "ring" version = "0.1.0" -source = "git+https://github.com/w3f/ring-proof?rev=8657210#86572101f4210647984ab4efedba6b3fcc890895" +source = "git+https://github.com/w3f/ring-proof#edd1e90b847e560bf60fc2e8712235ccfa11a9a9" dependencies = [ "ark-ec", "ark-ff", @@ -16721,100 +16671,6 @@ dependencies = [ "sp-arithmetic", ] -[[package]] -name = "sp-ark-bls12-377" -version = "0.4.1-beta" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9b60ba7d8fbb82e21f5be499b02438c9a79365acb441a4dc3993179f09c4cc9" -dependencies = [ - "ark-bls12-377", - "ark-ff", - "ark-r1cs-std", - "ark-scale", - "ark-std", - "parity-scale-codec", - "sp-ark-models", -] - -[[package]] -name = "sp-ark-bls12-381" -version = "0.4.1-beta" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2cd101171d2e988a4e1b2320ad3f26f8746a263110c7153213fe86293e0552b" -dependencies = [ - "ark-bls12-381", - "ark-ff", - "ark-scale", - "ark-serialize", - "ark-std", - "parity-scale-codec", - "sp-ark-models", -] - -[[package]] -name = "sp-ark-bw6-761" -version = "0.4.1-beta" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94d66ba98893cc42dfe81d5b5dee9142577176bdbdba80ec25a37d8cdffdbd5" -dependencies = [ - "ark-bw6-761", - "ark-ff", - "ark-scale", - "ark-std", - "parity-scale-codec", - "sp-ark-models", -] - -[[package]] -name = "sp-ark-ed-on-bls12-377" -version = "0.4.1-beta" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37f6ea96c9b1cd4cbd05d741225ff7f6328ab035bda16cf3fac105c87ad98959" -dependencies = [ - "ark-ed-on-bls12-377", - "ark-ff", - "ark-r1cs-std", - "ark-scale", - "ark-serialize", - "ark-std", - "parity-scale-codec", - "sp-ark-models", -] - -[[package]] -name = "sp-ark-ed-on-bls12-381-bandersnatch" -version = "0.4.1-beta" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4db7a801260397cd58077befcee87acfdde8c189f48718bba1bc3783c799b67b" -dependencies = [ - "ark-ec", - "ark-ed-on-bls12-381-bandersnatch", - "ark-ff", - "ark-r1cs-std", - "ark-scale", - "ark-std", - "parity-scale-codec", - "sp-ark-bls12-381", - "sp-ark-models", -] - -[[package]] -name = "sp-ark-models" -version = "0.4.1-beta" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd77599e09f12893739e1ef822ae065f2f46c3be040ba1979bb786ae21059f44" -dependencies = [ - "ark-ec", - "ark-ff", - "ark-serialize", - "ark-std", - "derivative", - "getrandom 0.2.10", - "itertools 0.10.5", - "num-traits", - "zeroize", -] - [[package]] name = "sp-authority-discovery" version = "4.0.0-dev" @@ -17051,25 +16907,13 @@ dependencies = [ name = "sp-crypto-ec-utils" version = "0.4.0" dependencies = [ - "ark-algebra-test-templates", "ark-bls12-377", "ark-bls12-381", "ark-bw6-761", "ark-ec", "ark-ed-on-bls12-377", "ark-ed-on-bls12-381-bandersnatch", - "ark-ff", "ark-scale", - "ark-serialize", - "ark-std", - "parity-scale-codec", - "sp-ark-bls12-377", - "sp-ark-bls12-381", - "sp-ark-bw6-761", - "sp-ark-ed-on-bls12-377", - "sp-ark-ed-on-bls12-381-bandersnatch", - "sp-ark-models", - "sp-io", "sp-runtime-interface", "sp-std", ] diff --git a/substrate/primitives/core/Cargo.toml b/substrate/primitives/core/Cargo.toml index 9f1f0127d7cdc..b9607eadb583e 100644 --- a/substrate/primitives/core/Cargo.toml +++ b/substrate/primitives/core/Cargo.toml @@ -57,7 +57,7 @@ sp-runtime-interface = { path = "../runtime-interface", default-features = false # bls crypto w3f-bls = { version = "0.1.3", default-features = false, optional = true} # bandersnatch crypto -bandersnatch_vrfs = { git = "https://github.com/w3f/ring-vrf", rev = "f4fe253", default-features = false, optional = true } +bandersnatch_vrfs = { git = "https://github.com/w3f/ring-vrf", rev = "4b09416", default-features = false, optional = true } [dev-dependencies] criterion = "0.4.0" diff --git a/substrate/primitives/core/src/bandersnatch.rs b/substrate/primitives/core/src/bandersnatch.rs index 832ef6c77bb3d..78b7f12f9ffd4 100644 --- a/substrate/primitives/core/src/bandersnatch.rs +++ b/substrate/primitives/core/src/bandersnatch.rs @@ -60,15 +60,7 @@ const PREOUT_SERIALIZED_LEN: usize = 33; // // This size is dependent on the ring domain size and the actual value // is equal to the SCALE encoded size of the `KZG` backend. -// -// Some values: -// ring_size → ~serialized_size -// 512 → 74 KB -// 1024 → 147 KB -// 2048 → 295 KB -// NOTE: This is quite big but looks like there is an upcoming fix -// in the backend. -const RING_CONTEXT_SERIALIZED_LEN: usize = 147748; +const RING_CONTEXT_SERIALIZED_LEN: usize = 147716; /// Bandersnatch public key. #[cfg_attr(feature = "full_crypto", derive(Hash))] @@ -538,10 +530,7 @@ pub mod vrf { #[cfg(feature = "full_crypto")] impl Pair { fn vrf_sign_gen<const N: usize>(&self, data: &VrfSignData) -> VrfSignature { - let ios = core::array::from_fn(|i| { - let input = data.inputs[i].0.clone(); - self.secret.vrf_inout(input) - }); + let ios = core::array::from_fn(|i| self.secret.vrf_inout(data.inputs[i].0)); let thin_signature: ThinVrfSignature<N> = self.secret.sign_thin_vrf(data.transcript.clone(), &ios); @@ -567,7 +556,7 @@ pub mod vrf { input: &VrfInput, ) -> [u8; N] { let transcript = Transcript::new_labeled(context); - let inout = self.secret.vrf_inout(input.0.clone()); + let inout = self.secret.vrf_inout(input.0); inout.vrf_output_bytes(transcript) } } @@ -583,7 +572,7 @@ pub mod vrf { }; let preouts: [bandersnatch_vrfs::VrfPreOut; N] = - core::array::from_fn(|i| signature.outputs[i].0.clone()); + core::array::from_fn(|i| signature.outputs[i].0); // Deserialize only the proof, the rest has already been deserialized // This is another hack used because backend signature type is generic over @@ -596,7 +585,7 @@ pub mod vrf { }; let signature = ThinVrfSignature { proof, preouts }; - let inputs = data.inputs.iter().map(|i| i.0.clone()); + let inputs = data.inputs.iter().map(|i| i.0); public.verify_thin_vrf(data.transcript.clone(), inputs, &signature).is_ok() } @@ -610,8 +599,7 @@ pub mod vrf { input: &VrfInput, ) -> [u8; N] { let transcript = Transcript::new_labeled(context); - let inout = - bandersnatch_vrfs::VrfInOut { input: input.0.clone(), preoutput: self.0.clone() }; + let inout = bandersnatch_vrfs::VrfInOut { input: input.0, preoutput: self.0 }; inout.vrf_output_bytes(transcript) } } @@ -733,10 +721,7 @@ pub mod ring_vrf { data: &VrfSignData, prover: &RingProver, ) -> RingVrfSignature { - let ios = core::array::from_fn(|i| { - let input = data.inputs[i].0.clone(); - self.secret.vrf_inout(input) - }); + let ios = core::array::from_fn(|i| self.secret.vrf_inout(data.inputs[i].0)); let ring_signature: bandersnatch_vrfs::RingVrfSignature<N> = bandersnatch_vrfs::RingProver { ring_prover: prover, secret: &self.secret } @@ -792,12 +777,12 @@ pub mod ring_vrf { }; let preouts: [bandersnatch_vrfs::VrfPreOut; N] = - core::array::from_fn(|i| self.outputs[i].0.clone()); + core::array::from_fn(|i| self.outputs[i].0); let signature = bandersnatch_vrfs::RingVrfSignature { proof: vrf_signature.proof, preouts }; - let inputs = data.inputs.iter().map(|i| i.0.clone()); + let inputs = data.inputs.iter().map(|i| i.0); bandersnatch_vrfs::RingVerifier(verifier) .verify_ring_vrf(data.transcript.clone(), inputs, &signature) diff --git a/substrate/primitives/crypto/ec-utils/Cargo.toml b/substrate/primitives/crypto/ec-utils/Cargo.toml index 15a6e85abb967..e091385071c9c 100644 --- a/substrate/primitives/crypto/ec-utils/Cargo.toml +++ b/substrate/primitives/crypto/ec-utils/Cargo.toml @@ -2,7 +2,7 @@ name = "sp-crypto-ec-utils" version = "0.4.0" authors.workspace = true -description = "Host function interface for common elliptic curve operations in Substrate runtimes" +description = "Host functions for common Arkworks elliptic curve operations" edition.workspace = true license = "Apache-2.0" homepage = "https://substrate.io" @@ -12,51 +12,26 @@ repository.workspace = true targets = ["x86_64-unknown-linux-gnu"] [dependencies] -ark-serialize = { version = "0.4.2", default-features = false } -ark-ff = { version = "0.4.2", default-features = false } ark-ec = { version = "0.4.2", default-features = false } -ark-std = { version = "0.4.0", default-features = false } ark-bls12-377 = { version = "0.4.0", features = ["curve"], default-features = false } ark-bls12-381 = { version = "0.4.0", features = ["curve"], default-features = false } ark-bw6-761 = { version = "0.4.0", default-features = false } ark-ed-on-bls12-381-bandersnatch = { version = "0.4.0", default-features = false } ark-ed-on-bls12-377 = { version = "0.4.0", default-features = false } -sp-std = { path = "../../std", default-features = false } -codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false } -ark-scale = { version = "0.0.10", features = ["hazmat"], default-features = false } +ark-scale = { version = "0.0.11", features = ["hazmat"], default-features = false } sp-runtime-interface = { path = "../../runtime-interface", default-features = false} - -[dev-dependencies] -sp-io = { path = "../../io", default-features = false } -ark-algebra-test-templates = { version = "0.4.2", default-features = false } -sp-ark-models = { version = "0.4.1-beta", default-features = false } -sp-ark-bls12-377 = { version = "0.4.1-beta", default-features = false } -sp-ark-bls12-381 = { version = "0.4.1-beta", default-features = false } -sp-ark-bw6-761 = { version = "0.4.1-beta", default-features = false } -sp-ark-ed-on-bls12-377 = { version = "0.4.1-beta", default-features = false } -sp-ark-ed-on-bls12-381-bandersnatch = { version = "0.4.1-beta", default-features = false } +sp-std = { path = "../../std", default-features = false } [features] default = [ "std" ] std = [ - "ark-algebra-test-templates/std", "ark-bls12-377/std", "ark-bls12-381/std", "ark-bw6-761/std", "ark-ec/std", "ark-ed-on-bls12-377/std", "ark-ed-on-bls12-381-bandersnatch/std", - "ark-ff/std", "ark-scale/std", - "ark-serialize/std", - "ark-std/std", - "codec/std", - "sp-ark-bls12-377/std", - "sp-ark-bls12-381/std", - "sp-ark-bw6-761/std", - "sp-ark-ed-on-bls12-377/std", - "sp-ark-ed-on-bls12-381-bandersnatch/std", - "sp-io/std", "sp-runtime-interface/std", "sp-std/std", ] diff --git a/substrate/primitives/crypto/ec-utils/src/bls12_377.rs b/substrate/primitives/crypto/ec-utils/src/bls12_377.rs deleted file mode 100644 index 78f20297299b0..0000000000000 --- a/substrate/primitives/crypto/ec-utils/src/bls12_377.rs +++ /dev/null @@ -1,103 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// 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. - -//! Support functions for bls12_377 to improve the performance of -//! multi_miller_loop, final_exponentiation, msm's and projective -//! multiplications by host function calls - -use crate::utils::{ - final_exponentiation_generic, msm_sw_generic, mul_projective_generic, multi_miller_loop_generic, -}; -use ark_bls12_377::{g1, g2, Bls12_377}; -use sp_std::vec::Vec; - -/// Compute a multi miller loop through arkworks -pub fn multi_miller_loop(a: Vec<u8>, b: Vec<u8>) -> Result<Vec<u8>, ()> { - multi_miller_loop_generic::<Bls12_377>(a, b) -} - -/// Compute a final exponentiation through arkworks -pub fn final_exponentiation(target: Vec<u8>) -> Result<Vec<u8>, ()> { - final_exponentiation_generic::<Bls12_377>(target) -} - -/// Compute a multi scalar multiplication for short_weierstrass through -/// arkworks on G1. -pub fn msm_g1(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - msm_sw_generic::<g1::Config>(bases, scalars) -} - -/// Compute a multi scalar multiplication for short_weierstrass through -/// arkworks on G2. -pub fn msm_g2(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - msm_sw_generic::<g2::Config>(bases, scalars) -} - -/// Compute a projective scalar multiplication for short_weierstrass -/// through arkworks on G1. -pub fn mul_projective_g1(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - mul_projective_generic::<g1::Config>(base, scalar) -} - -/// Compute a projective scalar multiplication for short_weierstrass -/// through arkworks on G2. -pub fn mul_projective_g2(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - mul_projective_generic::<g2::Config>(base, scalar) -} - -#[cfg(test)] -mod tests { - use super::*; - use ark_algebra_test_templates::*; - use sp_ark_bls12_377::{ - Bls12_377 as Bls12_377Host, G1Projective as G1ProjectiveHost, - G2Projective as G2ProjectiveHost, HostFunctions, - }; - - #[derive(PartialEq, Eq)] - struct Host; - - impl HostFunctions for Host { - fn bls12_377_multi_miller_loop(a: Vec<u8>, b: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bls12_377_multi_miller_loop(a, b) - } - fn bls12_377_final_exponentiation(f12: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bls12_377_final_exponentiation(f12) - } - fn bls12_377_msm_g1(bases: Vec<u8>, bigints: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bls12_377_msm_g1(bases, bigints) - } - fn bls12_377_msm_g2(bases: Vec<u8>, bigints: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bls12_377_msm_g2(bases, bigints) - } - fn bls12_377_mul_projective_g1(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bls12_377_mul_projective_g1(base, scalar) - } - fn bls12_377_mul_projective_g2(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bls12_377_mul_projective_g2(base, scalar) - } - } - - type Bls12_377 = Bls12_377Host<Host>; - type G1Projective = G1ProjectiveHost<Host>; - type G2Projective = G2ProjectiveHost<Host>; - - test_group!(g1; G1Projective; sw); - test_group!(g2; G2Projective; sw); - test_group!(pairing_output; ark_ec::pairing::PairingOutput<Bls12_377>; msm); - test_pairing!(pairing; super::Bls12_377); -} diff --git a/substrate/primitives/crypto/ec-utils/src/bls12_381.rs b/substrate/primitives/crypto/ec-utils/src/bls12_381.rs deleted file mode 100644 index b2ec48a42fc53..0000000000000 --- a/substrate/primitives/crypto/ec-utils/src/bls12_381.rs +++ /dev/null @@ -1,219 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// 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. - -//! Support functions for bls12_381 to improve the performance of -//! multi_miller_loop, final_exponentiation, msm's and projective -//! multiplications by host function calls - -use crate::utils::{ - final_exponentiation_generic, msm_sw_generic, mul_projective_generic, multi_miller_loop_generic, -}; -use ark_bls12_381::{g1, g2, Bls12_381}; -use sp_std::vec::Vec; - -/// Compute a multi miller loop through arkworks -pub fn multi_miller_loop(a: Vec<u8>, b: Vec<u8>) -> Result<Vec<u8>, ()> { - multi_miller_loop_generic::<Bls12_381>(a, b) -} - -/// Compute a final exponentiation through arkworks -pub fn final_exponentiation(target: Vec<u8>) -> Result<Vec<u8>, ()> { - final_exponentiation_generic::<Bls12_381>(target) -} - -/// Compute a multi scalar multiplication for short_weierstrass through -/// arkworks on G1. -pub fn msm_g1(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - msm_sw_generic::<g1::Config>(bases, scalars) -} - -/// Compute a multi scalar multiplication for short_weierstrass through -/// arkworks on G2. -pub fn msm_g2(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - msm_sw_generic::<g2::Config>(bases, scalars) -} - -/// Compute a projective scalar multiplication for short_weierstrass -/// through arkworks on G1. -pub fn mul_projective_g1(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - mul_projective_generic::<g1::Config>(base, scalar) -} - -/// Compute a projective scalar multiplication for short_weierstrass -/// through arkworks on G2. -pub fn mul_projective_g2(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - mul_projective_generic::<g2::Config>(base, scalar) -} - -#[cfg(test)] -mod tests { - use super::*; - use ark_algebra_test_templates::*; - use ark_ec::{AffineRepr, CurveGroup, Group}; - use ark_ff::{fields::Field, One, Zero}; - use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress, Validate}; - use ark_std::{rand::Rng, test_rng, vec, UniformRand}; - use sp_ark_bls12_381::{ - fq::Fq, fq2::Fq2, fr::Fr, Bls12_381 as Bls12_381Host, G1Affine as G1AffineHost, - G1Projective as G1ProjectiveHost, G2Affine as G2AffineHost, - G2Projective as G2ProjectiveHost, HostFunctions, - }; - use sp_ark_models::pairing::PairingOutput; - - #[derive(PartialEq, Eq)] - struct Host; - - impl HostFunctions for Host { - fn bls12_381_multi_miller_loop(a: Vec<u8>, b: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bls12_381_multi_miller_loop(a, b) - } - fn bls12_381_final_exponentiation(f12: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bls12_381_final_exponentiation(f12) - } - fn bls12_381_msm_g1(bases: Vec<u8>, bigints: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bls12_381_msm_g1(bases, bigints) - } - fn bls12_381_msm_g2(bases: Vec<u8>, bigints: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bls12_381_msm_g2(bases, bigints) - } - fn bls12_381_mul_projective_g1(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bls12_381_mul_projective_g1(base, scalar) - } - fn bls12_381_mul_projective_g2(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bls12_381_mul_projective_g2(base, scalar) - } - } - - type Bls12_381 = Bls12_381Host<Host>; - type G1Projective = G1ProjectiveHost<Host>; - type G2Projective = G2ProjectiveHost<Host>; - type G1Affine = G1AffineHost<Host>; - type G2Affine = G2AffineHost<Host>; - - test_group!(g1; G1Projective; sw); - test_group!(g2; G2Projective; sw); - test_group!(pairing_output; PairingOutput<Bls12_381>; msm); - test_pairing!(ark_pairing; super::Bls12_381); - - #[test] - fn test_g1_endomorphism_beta() { - assert!(sp_ark_bls12_381::g1::BETA.pow([3u64]).is_one()); - } - - #[test] - fn test_g1_subgroup_membership_via_endomorphism() { - let mut rng = test_rng(); - let generator = G1Projective::rand(&mut rng).into_affine(); - assert!(generator.is_in_correct_subgroup_assuming_on_curve()); - } - - #[test] - fn test_g1_subgroup_non_membership_via_endomorphism() { - let mut rng = test_rng(); - loop { - let x = Fq::rand(&mut rng); - let greatest = rng.gen(); - - if let Some(p) = G1Affine::get_point_from_x_unchecked(x, greatest) { - if !<G1Projective as ark_std::Zero>::is_zero(&p.mul_bigint(Fr::characteristic())) { - assert!(!p.is_in_correct_subgroup_assuming_on_curve()); - return - } - } - } - } - - #[test] - fn test_g2_subgroup_membership_via_endomorphism() { - let mut rng = test_rng(); - let generator = G2Projective::rand(&mut rng).into_affine(); - assert!(generator.is_in_correct_subgroup_assuming_on_curve()); - } - - #[test] - fn test_g2_subgroup_non_membership_via_endomorphism() { - let mut rng = test_rng(); - loop { - let x = Fq2::rand(&mut rng); - let greatest = rng.gen(); - - if let Some(p) = G2Affine::get_point_from_x_unchecked(x, greatest) { - if !<G2Projective as Zero>::is_zero(&p.mul_bigint(Fr::characteristic())) { - assert!(!p.is_in_correct_subgroup_assuming_on_curve()); - return - } - } - } - } - - // Test vectors and macro adapted from https://github.com/zkcrypto/bls12_381/blob/e224ad4ea1babfc582ccd751c2bf128611d10936/src/test-data/mod.rs - macro_rules! test_vectors { - ($projective:ident, $affine:ident, $compress:expr, $expected:ident) => { - let mut e = $projective::zero(); - - let mut v = vec![]; - { - let mut expected = $expected; - for _ in 0..1000 { - let e_affine = $affine::from(e); - let mut serialized = vec![0u8; e.serialized_size($compress)]; - e_affine.serialize_with_mode(serialized.as_mut_slice(), $compress).unwrap(); - v.extend_from_slice(&serialized[..]); - - let mut decoded = serialized; - let len_of_encoding = decoded.len(); - (&mut decoded[..]).copy_from_slice(&expected[0..len_of_encoding]); - expected = &expected[len_of_encoding..]; - let decoded = - $affine::deserialize_with_mode(&decoded[..], $compress, Validate::Yes) - .unwrap(); - assert_eq!(e_affine, decoded); - - e += &$projective::generator(); - } - } - - assert_eq!(&v[..], $expected); - }; - } - - #[test] - fn g1_compressed_valid_test_vectors() { - let bytes: &'static [u8] = include_bytes!("test-data/g1_compressed_valid_test_vectors.dat"); - test_vectors!(G1Projective, G1Affine, Compress::Yes, bytes); - } - - #[test] - fn g1_uncompressed_valid_test_vectors() { - let bytes: &'static [u8] = - include_bytes!("test-data/g1_uncompressed_valid_test_vectors.dat"); - test_vectors!(G1Projective, G1Affine, Compress::No, bytes); - } - - #[test] - fn g2_compressed_valid_test_vectors() { - let bytes: &'static [u8] = include_bytes!("test-data/g2_compressed_valid_test_vectors.dat"); - test_vectors!(G2Projective, G2Affine, Compress::Yes, bytes); - } - - #[test] - fn g2_uncompressed_valid_test_vectors() { - let bytes: &'static [u8] = - include_bytes!("test-data/g2_uncompressed_valid_test_vectors.dat"); - test_vectors!(G2Projective, G2Affine, Compress::No, bytes); - } -} diff --git a/substrate/primitives/crypto/ec-utils/src/bw6_761.rs b/substrate/primitives/crypto/ec-utils/src/bw6_761.rs deleted file mode 100644 index 4ad26fc54b1e0..0000000000000 --- a/substrate/primitives/crypto/ec-utils/src/bw6_761.rs +++ /dev/null @@ -1,103 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// 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. - -//! Support functions for bw6_761 to improve the performance of -//! multi_miller_loop, final_exponentiation, msm's and projective -//! multiplications by host function calls. - -use crate::utils::{ - final_exponentiation_generic, msm_sw_generic, mul_projective_generic, multi_miller_loop_generic, -}; -use ark_bw6_761::{g1, g2, BW6_761}; -use sp_std::vec::Vec; - -/// Compute a multi miller loop through arkworks -pub fn multi_miller_loop(a: Vec<u8>, b: Vec<u8>) -> Result<Vec<u8>, ()> { - multi_miller_loop_generic::<BW6_761>(a, b) -} - -/// Compute a final exponentiation through arkworks -pub fn final_exponentiation(target: Vec<u8>) -> Result<Vec<u8>, ()> { - final_exponentiation_generic::<BW6_761>(target) -} - -/// Compute a multi scalar multiplication for short_weierstrass through -/// arkworks on G1. -pub fn msm_g1(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - msm_sw_generic::<g1::Config>(bases, scalars) -} - -/// Compute a multi scalar multiplication for short_weierstrass through -/// arkworks on G2. -pub fn msm_g2(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - msm_sw_generic::<g2::Config>(bases, scalars) -} - -/// Compute a projective scalar multiplication for short_weierstrass through -/// arkworks on G1. -pub fn mul_projective_g1(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - mul_projective_generic::<g1::Config>(base, scalar) -} - -/// Compute a projective scalar multiplication for short_weierstrass through -/// arkworks on G2. -pub fn mul_projective_g2(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - mul_projective_generic::<g2::Config>(base, scalar) -} - -#[cfg(test)] -mod tests { - use super::*; - use ark_algebra_test_templates::*; - use sp_ark_bw6_761::{ - G1Projective as G1ProjectiveHost, G2Projective as G2ProjectiveHost, HostFunctions, - BW6_761 as BW6_761Host, - }; - - #[derive(PartialEq, Eq)] - struct Host; - - impl HostFunctions for Host { - fn bw6_761_multi_miller_loop(a: Vec<u8>, b: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bw6_761_multi_miller_loop(a, b) - } - fn bw6_761_final_exponentiation(f12: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bw6_761_final_exponentiation(f12) - } - fn bw6_761_msm_g1(bases: Vec<u8>, bigints: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bw6_761_msm_g1(bases, bigints) - } - fn bw6_761_msm_g2(bases: Vec<u8>, bigints: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bw6_761_msm_g2(bases, bigints) - } - fn bw6_761_mul_projective_g1(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bw6_761_mul_projective_g1(base, scalar) - } - fn bw6_761_mul_projective_g2(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::bw6_761_mul_projective_g2(base, scalar) - } - } - - type BW6_761 = BW6_761Host<Host>; - type G1Projective = G1ProjectiveHost<Host>; - type G2Projective = G2ProjectiveHost<Host>; - - test_group!(g1; G1Projective; sw); - test_group!(g2; G2Projective; sw); - test_group!(pairing_output; ark_ec::pairing::PairingOutput<BW6_761>; msm); - test_pairing!(pairing; super::BW6_761); -} diff --git a/substrate/primitives/crypto/ec-utils/src/ed_on_bls12_377.rs b/substrate/primitives/crypto/ec-utils/src/ed_on_bls12_377.rs deleted file mode 100644 index e69dbd7984645..0000000000000 --- a/substrate/primitives/crypto/ec-utils/src/ed_on_bls12_377.rs +++ /dev/null @@ -1,56 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// 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. - -//! Support functions for ed_on_bls12_377 to improve the performance of -//! msm and projective multiplication by host function calls - -use crate::utils::{msm_te_generic, mul_projective_te_generic}; -use ark_ed_on_bls12_377::EdwardsConfig; -use sp_std::vec::Vec; - -/// Compute a multi scalar mulitplication for twisted_edwards through -/// arkworks. -pub fn msm(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - msm_te_generic::<EdwardsConfig>(bases, scalars) -} - -/// Compute a projective scalar multiplication for twisted_edwards -/// through arkworks. -pub fn mul_projective(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - mul_projective_te_generic::<EdwardsConfig>(base, scalar) -} - -#[cfg(test)] -mod tests { - use super::*; - use ark_algebra_test_templates::*; - use sp_ark_ed_on_bls12_377::{EdwardsProjective as EdwardsProjectiveHost, HostFunctions}; - - struct Host {} - - impl HostFunctions for Host { - fn ed_on_bls12_377_msm(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::ed_on_bls12_377_msm(bases, scalars) - } - fn ed_on_bls12_377_mul_projective(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::ed_on_bls12_377_mul_projective(base, scalar) - } - } - - type EdwardsProjective = EdwardsProjectiveHost<Host>; - test_group!(te; EdwardsProjective; te); -} diff --git a/substrate/primitives/crypto/ec-utils/src/ed_on_bls12_381_bandersnatch.rs b/substrate/primitives/crypto/ec-utils/src/ed_on_bls12_381_bandersnatch.rs deleted file mode 100644 index 7e9cd87e543d3..0000000000000 --- a/substrate/primitives/crypto/ec-utils/src/ed_on_bls12_381_bandersnatch.rs +++ /dev/null @@ -1,94 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// 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. - -//! Support functions for ed_on_bls12_381_bandersnatch to improve the -//! performance of msm' and projective multiplications by host function -//! calls. - -use crate::utils::{ - msm_sw_generic, msm_te_generic, mul_projective_generic, mul_projective_te_generic, -}; -use ark_ed_on_bls12_381_bandersnatch::BandersnatchConfig; -use sp_std::vec::Vec; - -/// Compute a multi scalar multiplication for short_weierstrass through -/// arkworks. -pub fn sw_msm(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - msm_sw_generic::<BandersnatchConfig>(bases, scalars) -} - -/// Compute a multi scalar mulitplication for twisted_edwards through -/// arkworks. -pub fn te_msm(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - msm_te_generic::<BandersnatchConfig>(bases, scalars) -} - -/// Compute a projective scalar multiplication for short_weierstrass -/// through arkworks. -pub fn sw_mul_projective(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - mul_projective_generic::<BandersnatchConfig>(base, scalar) -} - -/// Compute a projective scalar multiplication for twisted_edwards -/// through arkworks. -pub fn te_mul_projective(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - mul_projective_te_generic::<BandersnatchConfig>(base, scalar) -} - -#[cfg(test)] -mod tests { - use super::*; - use ark_algebra_test_templates::*; - use sp_ark_ed_on_bls12_381_bandersnatch::{ - EdwardsProjective as EdwardsProjectiveHost, HostFunctions, SWProjective as SWProjectiveHost, - }; - - pub struct Host {} - - impl HostFunctions for Host { - fn ed_on_bls12_381_bandersnatch_te_msm( - bases: Vec<u8>, - scalars: Vec<u8>, - ) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::ed_on_bls12_381_bandersnatch_te_msm(bases, scalars) - } - fn ed_on_bls12_381_bandersnatch_sw_msm( - bases: Vec<u8>, - scalars: Vec<u8>, - ) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::ed_on_bls12_381_bandersnatch_sw_msm(bases, scalars) - } - fn ed_on_bls12_381_bandersnatch_te_mul_projective( - base: Vec<u8>, - scalar: Vec<u8>, - ) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::ed_on_bls12_381_bandersnatch_te_mul_projective(base, scalar) - } - fn ed_on_bls12_381_bandersnatch_sw_mul_projective( - base: Vec<u8>, - scalar: Vec<u8>, - ) -> Result<Vec<u8>, ()> { - crate::elliptic_curves::ed_on_bls12_381_bandersnatch_sw_mul_projective(base, scalar) - } - } - - type EdwardsProjective = EdwardsProjectiveHost<Host>; - type SWProjective = SWProjectiveHost<Host>; - - test_group!(sw; SWProjective; sw); - test_group!(te; EdwardsProjective; te); -} diff --git a/substrate/primitives/crypto/ec-utils/src/lib.rs b/substrate/primitives/crypto/ec-utils/src/lib.rs index 22e24e61f7b35..c5cc85077391c 100644 --- a/substrate/primitives/crypto/ec-utils/src/lib.rs +++ b/substrate/primitives/crypto/ec-utils/src/lib.rs @@ -15,251 +15,272 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! The main elliptic curves trait, allowing Substrate to call into host functions -//! for operations on elliptic curves. +//! Elliptic Curves host functions which may be used to handle some of the *Arkworks* +//! computationally expensive operations. #![warn(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] +mod utils; + use sp_runtime_interface::runtime_interface; use sp_std::vec::Vec; +use utils::*; -pub mod bls12_377; -pub mod bls12_381; -pub mod bw6_761; -pub mod ed_on_bls12_377; -pub mod ed_on_bls12_381_bandersnatch; -mod utils; - -/// Interfaces for working with elliptic curves related types from within the runtime. -/// All type are (de-)serialized through the wrapper types from the ark-scale trait, -/// with ark_scale::{ArkScale, ArkScaleProjective}; +/// Interfaces for working with *Arkworks* elliptic curves related types from within the runtime. +/// +/// All types are (de-)serialized through the wrapper types from the `ark-scale` trait, +/// with `ark_scale::{ArkScale, ArkScaleProjective}`. +/// +/// `ArkScale`'s `Usage` generic parameter is expected to be set to `HOST_CALL`, which is +/// a shortcut for "not-validated" and "not-compressed". #[runtime_interface] pub trait EllipticCurves { - /// Compute a multi Miller loop for bls12_37 - /// Receives encoded: - /// a: ArkScale<Vec<ark_ec::bls12::G1Prepared::<ark_bls12_377::Config>>> - /// b: ArkScale<Vec<ark_ec::bls12::G2Prepared::<ark_bls12_377::Config>>> - /// Returns encoded: ArkScale<MillerLoopOutput<Bls12<ark_bls12_377::Config>>> + /// Pairing multi Miller loop for BLS12-377. + /// + /// - Receives encoded: + /// - `a: ArkScale<Vec<ark_ec::bls12::G1Prepared::<ark_bls12_377::Config>>>`. + /// - `b: ArkScale<Vec<ark_ec::bls12::G2Prepared::<ark_bls12_377::Config>>>`. + /// - Returns encoded: ArkScale<MillerLoopOutput<Bls12<ark_bls12_377::Config>>>. fn bls12_377_multi_miller_loop(a: Vec<u8>, b: Vec<u8>) -> Result<Vec<u8>, ()> { - bls12_377::multi_miller_loop(a, b) + multi_miller_loop::<ark_bls12_377::Bls12_377>(a, b) } - /// Compute a final exponentiation for bls12_377 - /// Receives encoded: ArkScale<MillerLoopOutput<Bls12<ark_bls12_377::Config>>> - /// Returns encoded: ArkScale<PairingOutput<Bls12<ark_bls12_377::Config>>> - fn bls12_377_final_exponentiation(f12: Vec<u8>) -> Result<Vec<u8>, ()> { - bls12_377::final_exponentiation(f12) + /// Pairing final exponentiation for BLS12-377. + /// + /// - Receives encoded: `ArkScale<MillerLoopOutput<Bls12<ark_bls12_377::Config>>>`. + /// - Returns encoded: `ArkScale<PairingOutput<Bls12<ark_bls12_377::Config>>>`. + fn bls12_377_final_exponentiation(f: Vec<u8>) -> Result<Vec<u8>, ()> { + final_exponentiation::<ark_bls12_377::Bls12_377>(f) } - /// Compute a projective multiplication on G1 for bls12_377 - /// Receives encoded: - /// base: ArkScaleProjective<ark_bls12_377::G1Projective> - /// scalar: ArkScale<&[u64]> - /// Returns encoded: ArkScaleProjective<ark_bls12_377::G1Projective> + /// Projective multiplication on G1 for BLS12-377. + /// + /// - Receives encoded: + /// - `base`: `ArkScaleProjective<ark_bls12_377::G1Projective>`. + /// - `scalar`: `ArkScale<&[u64]>`. + /// - Returns encoded: `ArkScaleProjective<ark_bls12_377::G1Projective>`. fn bls12_377_mul_projective_g1(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - bls12_377::mul_projective_g1(base, scalar) + mul_projective_sw::<ark_bls12_377::g1::Config>(base, scalar) } - /// Compute a projective multiplication on G2 for bls12_377 - /// through arkworks on G2 - /// Receives encoded: - /// base: ArkScaleProjective<ark_bls12_377::G2Projective> - /// scalar: ArkScale<&[u64]> - /// Returns encoded: ArkScaleProjective<ark_bls12_377::G2Projective> + /// Projective multiplication on G2 for BLS12-377. + /// + /// - Receives encoded: + /// - `base`: `ArkScaleProjective<ark_bls12_377::G2Projective>`. + /// - `scalar`: `ArkScale<&[u64]>`. + /// - Returns encoded: `ArkScaleProjective<ark_bls12_377::G2Projective>`. fn bls12_377_mul_projective_g2(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - bls12_377::mul_projective_g2(base, scalar) + mul_projective_sw::<ark_bls12_377::g2::Config>(base, scalar) } - /// Compute a msm on G1 for bls12_377 - /// Receives encoded: - /// bases: ArkScale<&[ark_bls12_377::G1Affine]> - /// scalars: ArkScale<&[ark_bls12_377::Fr]> - /// Returns encoded: ArkScaleProjective<ark_bls12_377::G1Projective> + /// Multi scalar multiplication on G1 for BLS12-377. + /// + /// - Receives encoded: + /// - `bases`: `ArkScale<&[ark_bls12_377::G1Affine]>`. + /// - `scalars`: `ArkScale<&[ark_bls12_377::Fr]>`. + /// - Returns encoded: `ArkScaleProjective<ark_bls12_377::G1Projective>`. fn bls12_377_msm_g1(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - bls12_377::msm_g1(bases, scalars) + msm_sw::<ark_bls12_377::g1::Config>(bases, scalars) } - /// Compute a msm on G2 for bls12_377 - /// Receives encoded: - /// bases: ArkScale<&[ark_bls12_377::G2Affine]> - /// scalars: ArkScale<&[ark_bls12_377::Fr]> - /// Returns encoded: ArkScaleProjective<ark_bls12_377::G2Projective> + /// Multi scalar multiplication on G2 for BLS12-377. + /// + /// - Receives encoded: + /// - `bases`: `ArkScale<&[ark_bls12_377::G2Affine]>`. + /// - `scalars`: `ArkScale<&[ark_bls12_377::Fr]>`. + /// - Returns encoded: `ArkScaleProjective<ark_bls12_377::G2Projective>`. fn bls12_377_msm_g2(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - bls12_377::msm_g2(bases, scalars) + msm_sw::<ark_bls12_377::g2::Config>(bases, scalars) } - /// Compute a multi Miller loop on bls12_381 - /// Receives encoded: - /// a: ArkScale<Vec<ark_ec::bls12::G1Prepared::<ark_bls12_381::Config>>> - /// b: ArkScale<Vec<ark_ec::bls12::G2Prepared::<ark_bls12_381::Config>>> - /// Returns encoded: ArkScale<MillerLoopOutput<Bls12<ark_bls12_381::Config>>> + /// Pairing multi Miller loop for BLS12-381. + /// + /// - Receives encoded: + /// - `a`: `ArkScale<Vec<ark_ec::bls12::G1Prepared::<ark_bls12_381::Config>>>`. + /// - `b`: `ArkScale<Vec<ark_ec::bls12::G2Prepared::<ark_bls12_381::Config>>>`. + /// - Returns encoded: ArkScale<MillerLoopOutput<Bls12<ark_bls12_381::Config>>> fn bls12_381_multi_miller_loop(a: Vec<u8>, b: Vec<u8>) -> Result<Vec<u8>, ()> { - bls12_381::multi_miller_loop(a, b) + multi_miller_loop::<ark_bls12_381::Bls12_381>(a, b) } - /// Compute a final exponentiation on bls12_381 - /// Receives encoded: ArkScale<MillerLoopOutput<Bls12<ark_bls12_381::Config>>> - /// Returns encoded:ArkScale<PairingOutput<Bls12<ark_bls12_381::Config>>> - fn bls12_381_final_exponentiation(f12: Vec<u8>) -> Result<Vec<u8>, ()> { - bls12_381::final_exponentiation(f12) + /// Pairing final exponentiation for BLS12-381. + /// + /// - Receives encoded: `ArkScale<MillerLoopOutput<Bls12<ark_bls12_381::Config>>>`. + /// - Returns encoded: `ArkScale<PairingOutput<Bls12<ark_bls12_381::Config>>>`. + fn bls12_381_final_exponentiation(f: Vec<u8>) -> Result<Vec<u8>, ()> { + final_exponentiation::<ark_bls12_381::Bls12_381>(f) } - /// Compute a projective multiplication on G1 for bls12_381 - /// Receives encoded: - /// base: ArkScaleProjective<ark_bls12_381::G1Projective> - /// scalar: ArkScale<&[u64]> - /// Returns encoded: ArkScaleProjective<ark_bls12_381::G1Projective> + /// Projective multiplication on G1 for BLS12-381. + /// + /// - Receives encoded: + /// - `base`: `ArkScaleProjective<ark_bls12_381::G1Projective>`. + /// - `scalar`: `ArkScale<&[u64]>`. + /// - Returns encoded: `ArkScaleProjective<ark_bls12_381::G1Projective>`. fn bls12_381_mul_projective_g1(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - bls12_381::mul_projective_g1(base, scalar) + mul_projective_sw::<ark_bls12_381::g1::Config>(base, scalar) } - /// Compute a projective multiplication on G2 for bls12_381 - /// Receives encoded: - /// base: ArkScaleProjective<ark_bls12_381::G2Projective> - /// scalar: ArkScale<&[u64]> - /// Returns encoded: ArkScaleProjective<ark_bls12_381::G2Projective> + /// Projective multiplication on G2 for BLS12-381. + /// + /// - Receives encoded: + /// - `base`: `ArkScaleProjective<ark_bls12_381::G2Projective>`. + /// - `scalar`: `ArkScale<&[u64]>`. + /// - Returns encoded: `ArkScaleProjective<ark_bls12_381::G2Projective>`. fn bls12_381_mul_projective_g2(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - bls12_381::mul_projective_g2(base, scalar) + mul_projective_sw::<ark_bls12_381::g2::Config>(base, scalar) } - /// Compute a msm on G1 for bls12_381 - /// Receives encoded: - /// bases: ArkScale<&[ark_bls12_381::G1Affine]> - /// scalars: ArkScale<&[ark_bls12_381::Fr]> - /// Returns encoded: ArkScaleProjective<ark_bls12_381::G1Projective> + /// Multi scalar multiplication on G1 for BLS12-381. + /// + /// - Receives encoded: + /// - bases: `ArkScale<&[ark_bls12_381::G1Affine]>`. + /// - scalars: `ArkScale<&[ark_bls12_381::Fr]>`. + /// - Returns encoded: `ArkScaleProjective<ark_bls12_381::G1Projective>`. fn bls12_381_msm_g1(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - bls12_381::msm_g1(bases, scalars) + msm_sw::<ark_bls12_381::g1::Config>(bases, scalars) } - /// Compute a msm on G2 for bls12_381 - /// Receives encoded: - /// bases: ArkScale<&[ark_bls12_381::G2Affine]> - /// scalars: ArkScale<&[ark_bls12_381::Fr]> - /// Returns encoded: ArkScaleProjective<ark_bls12_381::G2Projective> + /// Multi scalar multiplication on G2 for BLS12-381. + /// + /// - Receives encoded: + /// - `bases`: `ArkScale<&[ark_bls12_381::G2Affine]>`. + /// - `scalars`: `ArkScale<&[ark_bls12_381::Fr]>`. + /// - Returns encoded: `ArkScaleProjective<ark_bls12_381::G2Projective>`. fn bls12_381_msm_g2(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - bls12_381::msm_g2(bases, scalars) + msm_sw::<ark_bls12_381::g2::Config>(bases, scalars) } - /// Compute a multi Miller loop on bw6_761 - /// Receives encoded: - /// a: ArkScale<Vec<ark_ec::bw6::G1Prepared::<ark_bw6_761::Config>>> - /// b: ArkScale<Vec<ark_ec::bw6::G2Prepared::<ark_bw6_761::Config>>> - /// Returns encoded: ArkScale<MillerLoopOutput<Bls12<ark_bw6_761::Config>>> + /// Pairing multi Miller loop for BW6-761. + /// + /// - Receives encoded: + /// - `a`: `ArkScale<Vec<ark_ec::bw6::G1Prepared::<ark_bw6_761::Config>>>`. + /// - `b`: `ArkScale<Vec<ark_ec::bw6::G2Prepared::<ark_bw6_761::Config>>>`. + /// - Returns encoded: `ArkScale<MillerLoopOutput<Bls12<ark_bw6_761::Config>>>`. fn bw6_761_multi_miller_loop(a: Vec<u8>, b: Vec<u8>) -> Result<Vec<u8>, ()> { - bw6_761::multi_miller_loop(a, b) + multi_miller_loop::<ark_bw6_761::BW6_761>(a, b) } - /// Compute a final exponentiation on bw6_761 - /// Receives encoded: ArkScale<MillerLoopOutput<BW6<ark_bw6_761::Config>>> - /// Returns encoded: ArkScale<PairingOutput<BW6<ark_bw6_761::Config>>> - fn bw6_761_final_exponentiation(f12: Vec<u8>) -> Result<Vec<u8>, ()> { - bw6_761::final_exponentiation(f12) + /// Pairing final exponentiation for BW6-761. + /// + /// - Receives encoded: `ArkScale<MillerLoopOutput<BW6<ark_bw6_761::Config>>>`. + /// - Returns encoded: `ArkScale<PairingOutput<BW6<ark_bw6_761::Config>>>`. + fn bw6_761_final_exponentiation(f: Vec<u8>) -> Result<Vec<u8>, ()> { + final_exponentiation::<ark_bw6_761::BW6_761>(f) } - /// Compute a projective multiplication on G1 for bw6_761 - /// Receives encoded: - /// base: ArkScaleProjective<ark_bw6_761::G1Projective> - /// scalar: ArkScale<&[u64]> - /// Returns encoded: ArkScaleProjective<ark_bw6_761::G1Projective> + /// Projective multiplication on G1 for BW6-761. + /// + /// - Receives encoded: + /// - `base`: `ArkScaleProjective<ark_bw6_761::G1Projective>`. + /// - `scalar`: `ArkScale<&[u64]>`. + /// - Returns encoded: `ArkScaleProjective<ark_bw6_761::G1Projective>`. fn bw6_761_mul_projective_g1(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - bw6_761::mul_projective_g1(base, scalar) + mul_projective_sw::<ark_bw6_761::g1::Config>(base, scalar) } - /// Compute a projective multiplication on G2 for bw6_761 - /// Receives encoded: - /// base: ArkScaleProjective<ark_bw6_761::G2Projective> - /// scalar: ArkScale<&[u64]> - /// Returns encoded: ArkScaleProjective<ark_bw6_761::G2Projective> + /// Projective multiplication on G2 for BW6-761. + /// + /// - Receives encoded: + /// - `base`: `ArkScaleProjective<ark_bw6_761::G2Projective>`. + /// - `scalar`: `ArkScale<&[u64]>`. + /// - Returns encoded: `ArkScaleProjective<ark_bw6_761::G2Projective>`. fn bw6_761_mul_projective_g2(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - bw6_761::mul_projective_g2(base, scalar) + mul_projective_sw::<ark_bw6_761::g2::Config>(base, scalar) } - /// Compute a msm on G1 for bw6_761 - /// Receives encoded: - /// bases: ArkScale<&[ark_bw6_761::G1Affine]> - /// scalars: ArkScale<&[ark_bw6_761::Fr]> - /// Returns encoded: ArkScaleProjective<ark_bw6_761::G1Projective> + /// Multi scalar multiplication on G1 for BW6-761. + /// + /// - Receives encoded: + /// - `bases`: `ArkScale<&[ark_bw6_761::G1Affine]>`. + /// - `scalars`: `ArkScale<&[ark_bw6_761::Fr]>`. + /// - Returns encoded: `ArkScaleProjective<ark_bw6_761::G1Projective>`. fn bw6_761_msm_g1(bases: Vec<u8>, bigints: Vec<u8>) -> Result<Vec<u8>, ()> { - bw6_761::msm_g1(bases, bigints) + msm_sw::<ark_bw6_761::g1::Config>(bases, bigints) } - /// Compute a msm on G2 for bw6_761 - /// Receives encoded: - /// bases: ArkScale<&[ark_bw6_761::G2Affine]> - /// scalars: ArkScale<&[ark_bw6_761::Fr]> - /// Returns encoded: ArkScaleProjective<ark_bw6_761::G2Projective> + /// Multi scalar multiplication on G2 for BW6-761. + /// + /// - Receives encoded: + /// - `bases`: `ArkScale<&[ark_bw6_761::G2Affine]>`. + /// - `scalars`: `ArkScale<&[ark_bw6_761::Fr]>`. + /// - Returns encoded: `ArkScaleProjective<ark_bw6_761::G2Projective>`. fn bw6_761_msm_g2(bases: Vec<u8>, bigints: Vec<u8>) -> Result<Vec<u8>, ()> { - bw6_761::msm_g2(bases, bigints) + msm_sw::<ark_bw6_761::g2::Config>(bases, bigints) } - /// Compute projective multiplication on ed_on_bls12_377 - /// Receives encoded: - /// base: ArkScaleProjective<ark_ed_on_bls12_377::EdwardsProjective> - /// scalar: ArkScale<&[u64]> - /// Returns encoded: ArkScaleProjective<ark_ed_on_bls12_377::EdwardsProjective> + /// Twisted Edwards projective multiplication for Ed-on-BLS12-377. + /// + /// - Receives encoded: + /// - `base`: `ArkScaleProjective<ark_ed_on_bls12_377::EdwardsProjective>`. + /// - `scalar`: `ArkScale<&[u64]>`. + /// - Returns encoded: `ArkScaleProjective<ark_ed_on_bls12_377::EdwardsProjective>`. fn ed_on_bls12_377_mul_projective(base: Vec<u8>, scalar: Vec<u8>) -> Result<Vec<u8>, ()> { - ed_on_bls12_377::mul_projective(base, scalar) + mul_projective_te::<ark_ed_on_bls12_377::EdwardsConfig>(base, scalar) } - /// Compute msm on ed_on_bls12_377 - /// Receives encoded: - /// bases: ArkScale<&[ark_ed_on_bls12_377::EdwardsAffine]> - /// scalars: - /// ArkScale<&[ark_ed_on_bls12_377::Fr]> - /// Returns encoded: - /// ArkScaleProjective<ark_ed_on_bls12_377::EdwardsProjective> + /// Twisted Edwards multi scalar multiplication for Ed-on-BLS12-377. + /// + /// - Receives encoded: + /// - `bases`: `ArkScale<&[ark_ed_on_bls12_377::EdwardsAffine]>`. + /// - `scalars`: `ArkScale<&[ark_ed_on_bls12_377::Fr]>`. + /// - Returns encoded: `ArkScaleProjective<ark_ed_on_bls12_377::EdwardsProjective>`. fn ed_on_bls12_377_msm(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { - ed_on_bls12_377::msm(bases, scalars) + msm_te::<ark_ed_on_bls12_377::EdwardsConfig>(bases, scalars) } - /// Compute short weierstrass projective multiplication on ed_on_bls12_381_bandersnatch - /// Receives encoded: - /// base: ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::SWProjective> - /// scalar: ArkScale<&[u64]> - /// Returns encoded: ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::SWProjective> + /// Short Weierstrass projective multiplication for Ed-on-BLS12-381-Bandersnatch. + /// + /// - Receives encoded: + /// - `base`: `ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::SWProjective>`. + /// - `scalar`: `ArkScale<&[u64]>`. + /// - Returns encoded: `ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::SWProjective>`. fn ed_on_bls12_381_bandersnatch_sw_mul_projective( base: Vec<u8>, scalar: Vec<u8>, ) -> Result<Vec<u8>, ()> { - ed_on_bls12_381_bandersnatch::sw_mul_projective(base, scalar) + mul_projective_sw::<ark_ed_on_bls12_381_bandersnatch::SWConfig>(base, scalar) } - /// Compute twisted edwards projective multiplication on ed_on_bls12_381_bandersnatch - /// Receives encoded: - /// base: ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::EdwardsProjective> - /// scalar: ArkScale<&[u64]> - /// Returns encoded: ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::EdwardsProjective> + /// Twisted Edwards projective multiplication for Ed-on-BLS12-381-Bandersnatch. + /// + /// - Receives encoded: + /// - `base`: `ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::EdwardsProjective>`. + /// - `scalar`: `ArkScale<&[u64]>`. + /// - Returns encoded: + /// `ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::EdwardsProjective>`. fn ed_on_bls12_381_bandersnatch_te_mul_projective( base: Vec<u8>, scalar: Vec<u8>, ) -> Result<Vec<u8>, ()> { - ed_on_bls12_381_bandersnatch::te_mul_projective(base, scalar) + mul_projective_te::<ark_ed_on_bls12_381_bandersnatch::EdwardsConfig>(base, scalar) } - /// Compute short weierstrass msm on ed_on_bls12_381_bandersnatch - /// Receives encoded: - /// bases: ArkScale<&[ark_ed_on_bls12_381_bandersnatch::SWAffine]> - /// scalars: ArkScale<&[ark_ed_on_bls12_381_bandersnatch::Fr]> - /// Returns encoded: - /// ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::SWProjective> + /// Short Weierstrass multi scalar multiplication for Ed-on-BLS12-381-Bandersnatch. + /// + /// - Receives encoded: + /// - `bases`: `ArkScale<&[ark_ed_on_bls12_381_bandersnatch::SWAffine]>`. + /// - `scalars`: `ArkScale<&[ark_ed_on_bls12_381_bandersnatch::Fr]>`. + /// - Returns encoded: `ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::SWProjective>`. fn ed_on_bls12_381_bandersnatch_sw_msm( bases: Vec<u8>, scalars: Vec<u8>, ) -> Result<Vec<u8>, ()> { - ed_on_bls12_381_bandersnatch::sw_msm(bases, scalars) + msm_sw::<ark_ed_on_bls12_381_bandersnatch::SWConfig>(bases, scalars) } - /// Compute twisted edwards msm on ed_on_bls12_381_bandersnatch - /// Receives encoded: - /// base: ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::EdwardsProjective> - /// scalars: ArkScale<&[ark_ed_on_bls12_381_bandersnatch::Fr]> - /// Returns encoded: - /// ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::EdwardsProjective> + /// Twisted Edwards multi scalar multiplication for Ed-on-BLS12-381-Bandersnatch. + /// + /// - Receives encoded: + /// - `base`: `ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::EdwardsProjective>`. + /// - `scalars`: `ArkScale<&[ark_ed_on_bls12_381_bandersnatch::Fr]>`. + /// - Returns encoded: + /// `ArkScaleProjective<ark_ed_on_bls12_381_bandersnatch::EdwardsProjective>`. fn ed_on_bls12_381_bandersnatch_te_msm( bases: Vec<u8>, scalars: Vec<u8>, ) -> Result<Vec<u8>, ()> { - ed_on_bls12_381_bandersnatch::te_msm(bases, scalars) + msm_te::<ark_ed_on_bls12_381_bandersnatch::EdwardsConfig>(bases, scalars) } } diff --git a/substrate/primitives/crypto/ec-utils/src/test-data/g1_compressed_valid_test_vectors.dat b/substrate/primitives/crypto/ec-utils/src/test-data/g1_compressed_valid_test_vectors.dat deleted file mode 100644 index ea8cd67652d133010e79df8488452450b6f5cb17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48000 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 Rm?nepfGSPSF*XppvfIk?3d#Tg diff --git a/substrate/primitives/crypto/ec-utils/src/test-data/g1_uncompressed_valid_test_vectors.dat b/substrate/primitives/crypto/ec-utils/src/test-data/g1_uncompressed_valid_test_vectors.dat deleted file mode 100644 index 86abfba945c7b701750d637bffe6701330e10e88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96000 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%0A9l>2- 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%0DmNB)mjpnkUN?(=);js1H*V>?GsILXc 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 diff --git a/substrate/primitives/crypto/ec-utils/src/test-data/g2_compressed_valid_test_vectors.dat b/substrate/primitives/crypto/ec-utils/src/test-data/g2_compressed_valid_test_vectors.dat deleted file mode 100644 index a40bbe251d90e576060adb7eaa2456197878804c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96000 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 diff --git a/substrate/primitives/crypto/ec-utils/src/test-data/g2_uncompressed_valid_test_vectors.dat b/substrate/primitives/crypto/ec-utils/src/test-data/g2_uncompressed_valid_test_vectors.dat deleted file mode 100644 index 92e4bc528e8937a311b502e4940e5e363a1f7c57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 192000 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�uc*gWwz@d=S2~bA~LrFNXzUL(>hGG#V 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ƄhXNwSsyS zJU9w=e=S4G7JTjHh9TeCmHh?f&W++L+*qr=9OfmBCV?}{MtSoALw)L!lF5yr_zSYr z{)!Am3H^QDApa0)dGQ8?_mss!H1Vtc@oKiKu8dl0nI$YBZ3UlTpTDhZ{Q$)#y%&z! z44MrFy282&J!B}RY!owCmTHlsj=Hc_m-b>X|GxM9%Vw~jSK$WHc8q$O9xw6W0km-O z6Fow?09`Ioedc+C9nMPET%3E9{blbJO&U=Y(J|h?Imkf9uR(4_D0>8?4L;nb^XMlK zk=RsfwY3gx-Nxl_$I{@}Zy)t=$h&l-oAhw`Us~CBGh2V#o15IZ4GmRe2s~G&vc53V z7|X~f6y|{P1><*Qu@p{D!-=;|^I$446^_S?eegA85b5$d4SqrT^tNj(CH=%cFD6d5 zdIej>TFkh8eB1AaqxSjW|3)+cH1x#Q6ht(@G081r64T=l5Kd2nRoP;6-6Qm#*v@@C zPLYJN(zSJ;$&l{UV~EGP#e6KGAH0q;fuf`37FSh0vC4LWi6+??XxjG?i78I)9_urq zH@|@+rzkM+SIJ|`F)*&5k@>Em5irKM2ATXbn&H?!S3OT??4s4Wl(9%1L(}c)z)_Ij z6TZAPb$@TT^7qh*xj9E~QB__x6Qmk^;eLRtH;Lv#>{f!?<qKzHe^itwX?Y126_G9r zbQY&MiYN+wqK|rST<4Wb7vums-`PwT@!em5wmn2`)fl22qCe_dldV_dWi`jlT(5;r zBwzYEj;R<2b`DZ|3h0T7bfRNAG(ZB=;IQtD?E{?Hba>4urQ5QU^oPjy?&Lh$Md({@ zR`}uR_sww+73k$uH|u&ri{_(aCHqTk<0@@L@&5X9$&i}<l9Nl6cD6xT<Ra87*Qkt+ z?K?Q780!~;F<mlGPHsp1L+%-P9s_o$h=jkQbq17X_P!#Q0{exR<v+h~r0z~8?|bZ4 z0AERQ#tS9-X^j63VTCol;4>q;I7SWbp9(E_v9B_OJ^nFg=gIH)?Y)uM$_uQp3IuO? zf5T-l+p?~2qEmn5n4yGx_|nK=@ezpl%xshyF8t4dOGN_P`HqqnkhmF!6Y?9B8zjyE zAm;R{akIED&C3MIQ|M3BUcI6)taipOrVN}z)3MwW1{klM!k>l~0*e`hsG4W0%0DJH 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�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%0Dsk->ASDOX+DM<s$LHnxTE;T1mG-W+ zJoh-R^Trr~l-;zX{_5<=LnXCVl?FS$Q4?6mGZ`BYP;PYn%Zb4lI}<VM*~mhg?`5_x zavyG^5{g>5OmO|S>vB}Ib`O%dUI!2)Mh`#25!HnWEXzXbey~#m{is#^*o|{`+z6TG z;5_uxY!N(VnMp`a;t^}9hcLp(kW(gM%q~ISxgZ1fAhJItH4+!8IQ0M_xV+@@>QW%1 z+>0Fk08Ps|X$bSuud!A~DvAN|e{WA=xLrgM9~8FxYl=vRL-{s#vJu^wZEi%Uz6)LN zLdsr#gBA_Am7~49i}Gic-Pk3CEbC@u5A*>1wcrStw(h1ep;J~zF#5NRxm#a%wGbB1 zP!r5UA&hhdH^!5BAi_;bgTt(aiIYbf4R~Z#405<aB09v3P}GY;VVuWj+u)3P76NSA zu;rhQDGofnzp-}i0}2%bAsN}?RZ7o&*1HbeRF?p_Y|uUbMSxD~fsX)x(+GMZ0SDJf z<7i+yRK8!+e5;jXLwu9F3=0u>Eny6G+b|Ihwh<&0vuXHqpfrP&uLK`R^1<JOR^>1z zO#pq;DEEf+1xv!WO#tPa1y|XluBBJIQ3ey022E$;MZa;N-T}DldMG(u1JgkCZ8s81 z>CGc8F*VU#<pz#?vtTBAMiSjRINGV*vt+Q(S9fvrj~8TTYs>bfa_lmz0n_h7{Ps0? z(TpJ7l!Y1vzp4%}?Fm1Q<_4WjT3ocb%)K_)92dTo%nMJkB(w)4W^*eg%zXtn1}`SN zh@;E@mkxp+EU#C)h94PQ5D<K@u$<@UloX={YZ<3)A9L*@nj_}}7<~|+UXdb@(Bo~b z0S#sEL$|wVx|+5BCA|bP3%-=d6%&4*Ou6T|9AXXG)4?pR<Tr#tMhH9i)Uot7nGlp> zBhlQj8M}+b@@M5w)(ZyQ${5l`s=+`gQ9A4jp@rhS2SR&mXEcoCpt-}a%^uspbZWrW z=~Q~BKhkLNa+7B!s}`iiX#~T5LdJb6WBO&{!$Ka6iV#IvQZFGJF$Ph{n|(U~#~cPl zUwW)W<TTS$&;$*Qo%#Aq%>$$WG;bd_BlFQ{<QiFdYg>m4;Ai8a`GiHL96>>k^N-uR zRevzk<rd;)e!fv~Bpr%P<cdDE0fF!9Xj9+IlD37aR=phuqQU#_&GeT>f{yC~m+qi| zG!S}e2HX!-RK-fmn+RS1(}G29=~Gq1_`TJxs6=%eHr)?b%CK>{E998o1YlTh_yBh7 zwq7KsSGgf28>`Wh=vAf-arsTuxg)e^Ps5srQVP23*x<m3))|l=lx2WSCl5l7a2JUV zFHK7<MXl!5fPuOokCm<D5G>nuW~L4rGqcBiUfdI<({DX!o~$cf@d<)Hf$HM>Chn^# z=B%qRUtc*ou<v#toF)hAAlmT7TvMuZI<luf1&zNtJJ6^u9S_@dhq<JkYC5M~Sn5e} zP{S)T+&KGi+Z{5F#*)WcVvUKXH=qEuQ|K3g&3tPBNfQc`YuOFt=~!caO2rU>hzs$) z$~h&nbogU<f1W=x!+ctMmTj#DH%~1Ai9fU`PXtgMF>C4~UZPBZ1<%WHMUY_yP8@h0 zVKSoB2ZFl%R_~xDacxnB!cE3Hwhzf%5C*TT!KVDWKFOapEZAJW@g>&=$2?N?CzB{E zHo&=sZO`e~!YtJ{<tl?+lu%2UBo_X}!gglFy7{~ZS!%nLT@K-t$b%f_Cl(*wmvd_u zy$wA6^3U1T=+y34=Sr1}!2#Z+`xoOdzv+%S)+>zlu0$9(CWjEALHsUuJ2`~@3eAJV zFQ}3fJONwj#|A`T2MK47KmV9wwFH#8fTyYq8{b%u`YtHNE~`){l1R{-WDVVo+D$R5 zV*8cT?0;+Ioe$#()%r7EKkF(-@DJ_~emhkua|V~no&zunJrS4#pVBbkPQtRtK>I!Q z7&5smAO$?5h0>Q8O0x}pwn>MG<kwC>l}Ccv1w$+`#l%La>aIL&Gc6loTep9!EUiHY z@)z*cf~EqRx@jw-aIYsZywvlG2rsYFyJ{{i2{?dgk3YROVT_I-sT5YG*pb^k0|}z( zg9-^H=e@Bb%EFybO<*p#Ik9g@lZf|j8*yWswF8=TKab@mGHAPd9O}-v{}EavH&iik z8T`zVda(PBS!=mq%-PW$EOfLW+b?d8%HQn((2)FgJ)i#tl{~&)iUtd+?4(Rf<8P?d z$h?(WmtR?r>Ujo04fzOu3o@S*K1V!8;!QQ6RxE4yFVVJ7assoPj7lx%TiMWhEb>r% z_1<QA8Tno8NQa87h+8alyJ&UcDj$XJI028E<F7Z^g%f&>q$S??t~!3|hjR9@GTv5$ z^w1(GempyJN6ASEmZ<J_(zr(S`j6}Lx-%^1bPNTM#xd@hdSFpRMApSr@Ns_)!sm($ zIOV;sXyvR!q_|LOa%x#TrB?aAcNvt|ix|PRoK~2ByHp<L4vx$wOF5+r!7WGj6=R(L zck_$IRUQ3|lv3ckU^)v68LSxC)e#Q1=#97!G)+?+uGnMje6Q&jfYC0=!YCrT*d7ge zZlD5oUGN_6YbmO^RzQ4ddkz=e4rml8w6A}H2at)QI#SJe({=wrh-$vGkJ~P`#N5-Q zcAD99MXk2sIaaMztrVZ)#ZL(BS4}$JF@jpsM9VOVo>6$Sq@}syF&6O<j>@Qa3*tQp zUgn;)@+(C@2oOJD=$?patFT&efJg}GVtjFf)C!Ni1Dsk-^s%dGXbP*=Fta@WKj$?? zwnep<{}N518SO$vM3pp;-qse>+=7>PB^Pvbfaj@DRK+d;9Yq|8u5)K|=|1&O#Ebwr zCl4JkP~uH)v8=pf=1=2o{*9p3vP(D1G^Bu&F=s&(LgKE#>o_)`1Cs)>ArW2+wGg8b zGET7TH#JT{(X-YdzjWE~yer_cTfhg7<F1KXv(a{>AVw^tdDa)=*X0#_X9(pjv0M(? z6aI-JfST)D7+z|4!o`Ggy(2mWeIFWPgBe)3D~-c$GUWQIh<}w$?F3OBrQ7kAM)o_@ zwhQw%`@6Dvi>nKp$4rCWC9s9qF!iuuqc((%OUT2@NynSAaR*(LfvvYlNCwO5;*1=) zS0B3nzql88B-Uy`lKR<Nt2L979{+ERpzukw*~jMSF%XZG;Vt~uV05O1*u8gTxA3X7 zekoVw%oE6`yE9O)-jR4ORLywwQbr|>*x$?sZUM1+_igAEfPHs&%dqVsrY-u1u@?X{ zl?=b1Hn7?FX!&3U8huTfOl8b1M@^L1$r)*akHc1fId9Q?%YKfoFrvYIFE~IQ7~XHz z#NJX!Gu`e@Q4&k!LgmG3?pHK-)&tdwm9z8;jdv|pORKRF7cQMetDYi;5BDxkKNwU} zGqL(8OccU~K(sI1KH9#(n-Pd>RJ@yupKLiNa{CK|x*S?DU#>=d%0zyP*}T1%dt?Mu zsr?Sqv4$X)mzTWQR0*IVt7e%h?V(S8PzSYTB$U+fImC;MF_h4+3=C$sO~id0Wj5@} ziD?OX<v()?1PuFJNN-CqYc$Eg9luU$d+QGqrzeQ!L#$bf{cVJe4cH5-HtenK?V(Ao zb=yX4>IV8*|HVVXv3n)oOC2lMw*U^g&FjSxd4Qz<KkGo{mP3+6XYL)%@gC9iLR!vR zk_nQUn(IXKwmkS#&a_aF*=|VZyJ0A%yoxbG{pl4{luusB7!jkk5EN756(%c$su5OI zu20+@SVCM85G-lRrJX0`4$$m$C6WEV)YF%-`s>IdgLPbpe7ff+q3?s1g$e8naKN|G zRF%jLzRmejnbg#T%H+(=T+3EF62S8$>1bI^)ZAw~LebT-OvQDEj|;?3?|2}XwC0+E z$)9b`_tqWyIe;md*wmbD1#b})_EW!y&^*`-G$ia<d%(j6`~$bjX{|z1GO$MfThqgK z1C}F*8@VZ?zm&SS91&YU&_R2%Q?YI})3CN1aHKfWUm0-Q`s>Xd4F|W7I%Lh~$hWx@ z$!WEhbwXz+yzCG@;{C#fV4HE3>p;KI#!s*Z)fAu=lXiBT#-jmVS}IwaVd^}Q8_0v! zu+Jj3*|h{zkT2ojJ{e8WZV*dL%%b$YW(>!TZMiO@(GI3O8~F(9>QP##tMH<#IA%v; zZCL%BHt^>4&yzQ3`E`J?$ujktunf^kj4TRwvrm9P+KnPkuI8-tHTp5TK$aApKRfHB zEJACmT-!n{yzczPmk6>3;1KHt&+Xp+a<uwkw7$Zn=$?2|n78rJPX3po;H6*VZ?-fZ zWdTz7f*XgzN6JKWya#0*5-u|+8WDK&th(T_-=o`wjd)?@Wrwn&+BN2T)bfE<RoEP4 zbJ38zFieyhIun3EDA~d2?`4MXv+<=IMaiyl?5Hg~tvVHG5~MaxEs~9dOAi}bir0WA z7YS%0PZgE0)Gw^DE<oI<j4DIQ?oK_Rbbcm5ns`0e*b$d#a{xr@r3Sq7=e)F3V;uqZ zvJr`$-iP7$+sr*RO@|LuB5+WwJ^)o;wOHn~Mt5}#bH-!;l;%xEAzY8d>wq*2%NK$u zWVfr5Eusq6K>d`xNkmr+;5o4jEArrBx&vm?(_SIYyg4cGnG^>2@6KWqQx{5PS3J16 zH)}&erWbEmPyls*a)vjl;t=&-yEKsX^her9C3LcHiinBWkY3A5Pz4tOkI{TfiM~y9 zzB*<3>p%cSK)S!%HDW6*Vi23zPCZfy&n5u!XR0*N1Wq_KN=>HW;R6zW6MRavfYf7; zI}f3q%GGwv^34pc?Ia^>72_6j2b>6Rx92OXV|3@8m8}x_Y4sEQZ#42~78dd1=B&72 zTp?tk((%PAo}l$e#dW3jF#}x*yrk^$0idR$Y5%SO?KTE6DlR}2Fx$bSw)bz{061yt zi(1nvp4&qt=ut&UQ1(6Q6bP_1*7y;)gCw8tN9Yb$`kXnNViWtIHKj5lZ-%($BP%(Y znc&MPZi*(q%ChS_()pIp!!`+=ypXtgkkt}2!H%5KTFdaI2ORXM)L%eBU{$0L#rNp` zY{qKY1=uv(<xK5l_=Z<91A<kC=`s!;z3-IlJ3eY-Ku2Z+;YuMFIgTjiW_*N{@oR0i z213R>cVK)vf*Hcui+fxfq1FJ2QI+LbUox$l@Do|#LIQibNg~90eyb;wVdt1VP&2QJ z%LV)bs}NV6k86}3@p%ULE2Pm!=(j+&!yE=>bO!S1E7=;<JF*`3y_9X=txJ+|wkZqT z&2Qu+D?D5%OmYdTO+LXQ{ikEA6DjJUU$zt;q8~gPH9da~rQVv;f<V@Tol|VM_?mdp zDqXE?QJoEFx@&a{fJig)BeHIhrvPGta$e88k^e(*i*>ft%Z5NJBNRX=8RW34cbNr5 zC(jm?YY!nr1Vwiab=@?}_;evDQVw$%_IX*U4%ZvU`Q-k5Gt<(oDpuU8*TOs{D<Kb( zgZ74}S&2xMjQhG5M*y3Y8b0zYN#9hq7fO$*U8~9yV~|k+f`a0Z_WJGjNG1R*t$1Xz zNpx&6q2cGe1ukPp4i}-v{16(9fZV=R5ri45%&sL+Nx*Ie9xAb?6qf^7%j>4GVsHyJ zL4okidj2Pc5W#@OEW=q~hN!rj&aQz~4*no3_%h!C&0o9GT@4LW9VFOCOlDtyDZC}u z$*s@E5!K|&d||pabZV$-5#XZUx`~bKT2gONjM+4j0a6*YBSSdh3_yL^S}7e0@QI4S zM8rF?#K3nC0@26qbs~!<Z)2!yw|mQ$&NRfTp^p%~iCj<R>|*Wp|KpS7mp)P0g$Uvc z$rY$qs)iqyk<u$zMCC3sJ!YMsYDenq6rC1-ik$FAla2y5&pmQysK)S5uP-p}Mlh<# zSksJUJrd=NTq+@nq)%GZ;-*ou2-p=cK^Th(EeU*^l!`_C_kSns?WSyqAGtX<m|d5x z`>SwmBVuBhFa&(^aje5<HuDabIC1ZfmS#d6>$Jp7m|?T~oR!F{6Yr<20(XIi*8$ca zEh)%>!@HR5qx7%68b$&f31j>*W&9*N8Y(?!_DdtuEh-WOq$vfQqX&vM$^;a2r5Z3) zre5($87BaE-?j-aGLgq!Tov!)KG+F+{s1~cSM39&oC)Ye-|G^8OgP|@27e*LKGhy< zpr7pEgYF0<L7qG~DjurHt0-BXe7F*HQ098X4svo*zHv6bnLUus!!g&mBM#DXp+jWp zRjwGnmE1jPR<O!VONEt!h-7z|S(9CfWTV7hbc~U}S<DT>%FL*#Kx?k{u(##H*~bML z*FB1c(e{Ec+`|G5yBBl9MppAflnb=k39J7MCQjq_aQD9#n&z3Rp!{%v5C{_LPA#JZ z;EB64lqg8LjPOCCiHRez%{iU$-}Y{HW^91f>qwZ+CpAEqaa=xqOSTWQ*PGR7Zb3Hm zKG}ya20e*>JKHx?ucK<hlp!XMp5v;jO1WN=JhAG7R=yPY4_FNW!gN>!&`hGyL-XQi zY^~vjd}yDQl5o9=EjS03<b3+aEko)<*6(T&PHc_i#V-U8B17MpnK_}i3#hw7CGoL8 zjgM1w-ucQs1X^VW+q1~H$)^u(X!|S0yyPA50wDrPqIOjX#z$J}v=;2<!g@y*X`jGS z^x!{=1v1XCi=MDX@R^I}Qa$hlV0Igm3@-%Y{*xE5tVII`6_H0-7jDL|)c6IM=eq%6 zK4QvCFyzjYaPL{KaG<(n(rS!o$=VN7G23muR`5oFJ8Hhx4j}6*HR|x(MR{$AZW&q4 ztI4O5yO5v81voiy+MkDeko^ct1)p48o18_=kU>ECQ4DY8IZtr0G%TJ7rP3NEB0gRn z@bMP>VnbWwS0Q@U^FtI3*8+$xPi+=eu`B*Jo)mDvs~5f$i#I=Qu=*$HhFX+K+mU6M z2Ky5(Ax>0iXc7Xwl1XyLP7*f=w?CZIbVco1@`#N_Ha6ykT1-HwHLMBJe70@>d-;XD zR%hp_lXw{5vLVX$Y{8-bmp~lAIFxT3A}Lj9#;D~svycHtaQfBzBP-2l3*QtViHrh2 z^kElrO9y-aeVE7!Jr6)IMYQK!%GfXkYmIynhP?OzTuA_Cywc3wS$qsZB(6fZs;m~b z5KgE>z5zL(T1!7|=m%2%P2Tbb5|?%iJLkKVZJzUH`J4<fy*;D|S!k>?nAH-3NYKgs z3NI!nAqwyyjV{|4R}~eX6wMAMt_sMSLe?D{0nK+5MbE0k3mFLEL`oEL8DMWGkl@Y~ zji#%1QpZdmQg~tp%!Ov@^WmVi`k<E~#}bs(GiBpK(cPXJfM^NMN!JvBZ~Ly+;k`O7 zG&pKCpFu#zi)<oEAdK5nlNLVEW(ov~jNIIIQ?Ww+Nh1-YGpGudpU>1v?>y7IaliGs zCZrzyc8g=7gI<kIrYf5WD|&r3nipo_4;Ub2xS1BO*w=L8gVry30UvC+3KvY7Rg{{0 z09y#l#sD1cX!Gnp;jNJ`i&dkG1C)|hj3gMbvVQJa?E!!ja+gs>S!Md>W5xA_7!;D8 z9o7$t3g|9a#M=*K6;F>#4T7;RcM$=OhfBLzN-b5Ei{YG^LeEij{;+ob?kYx~53Z&p zk(k{&)<sQ6r&1n>3D^T}7w#8J7><2j%_yxCsZ_atxj%t9IP~kkaKD-9&d^~ri7F3w znAC9BK9Ym2v~p3f_+JWKKdfz{o3tr_Y>?#z)A*3PKqTRf^YACkq`{4^FVQr!gR3$n zz6EiZw68rO50(YvJ+tHXa?SS~pO@Pi#6@DgHwvdr{Pnt1O%=VGb28AaYK*nMzyT;l z3Wu?Hq!kc-3Sa;L&e@0OthP%F`O>2(s4tu4X5E5pb@c0tOl1)3#fEhQ6SHVLD;=kS zI71aDDPN%ook@Pg<`uxV;0|@UvEMk7t3}`DJ9u!x5U9N^6uWeZs!>QeotM7A`4<dj zA@Qm+T4JQ^Ozy(w|9ijsI0;vt9+WSHxcsNC6I@WVXek<Qj#a%f*H)L0<dy>&VnrJ> z8u~Ri&1*k!8DU*70TX|3?;KO>1kdB!d!rb*`o}uNp9?vUnov+Fp>_{7Yf@#hPs8wN zK=fhPdRV+oD<pWefk9Zo>F_I$5&ttU^1$gWArZq0c!9@3u4)zW>L<eF&k<(G2!?$O zmpoq{wmJ!l4l;{`!_^gk)*os+Lq|%F4(BD2f2*AH!j%OCd%OP=pemXq3}KeR?j?TB zpq?e~cFC{e#A7GSEU1^o|C7Gz_58<vX{76&5*r6uJwgFg8-_Ckk=|8T3eL(zt<ojz z>*&+0=8RUaclo^raZG){VTei%yT#K$tH~KKODf`8=z>qgzG#Nd<D6?K1JoHJb?C=& z8O&PyQB=DQ^}6>`hx{GnI8NawU4au0_N|9=I0jTB1Mwr3^cxW222wL%$PZ#)?uMDB zp<ur6E(@9`JGWelrL13AYF-c;ofXS;i;_vw_0%=?Gvw)|2uz}n7{AOvRyT&)RW13? zZu<j74W`f3`%Ak8l>iLW==2(@3oap-We|HT=SE%A&-{rJ91?*P1>`A;!im34uN6BM zx4<7(opcx)Q2-YQOeHvDzh_xD3*0=16U*imFz96Vi20<@G<;P|6qxvCBVBLsi};U3 zFtD9dDSQwK+#WpHNZX_)7VSoDl3OAuMe{I1o}l?nh0QWVU9P9*dDdpzJ?9lboxk9W zk~j;;uGURWs=Q|gjS`>J$GeALnq++CG9}6VzA`O`SX>BizQJGbTf7NJo8c+<^)3@u zJ!&ysAh~vB62l%2*ydEsDsO`ii5YXKfQ1cgNJ+jcuOKtFXbTlhN9e62Zx{t8wDcxu zi{dIFIhbKxhoW`FAd@&zv7hv4Pc)?d`9+F9KLj&wNNHU)2D?eelI#}tH@?1#*cr|m z;h+@vBY#^=ua9lsEPdXJU=d1~0KOJ_k*h7g-F8-TdBl^4@4pEJ0&<!shmW2YTivS3 zF>DYX31>uWZ;-&QT$AD{Hstl$TbkjZr5wl~iXbDQ43Yv6eZY$`d`i)Cv1411^d0lA ztKsn<Nb?p7BTDmQ$6wH_fgS<OAY{Z!OCyr&>)HaV+Dq`$(QImpgQ}$*<b&TpLHPYK z3;1({Qs*IzlJg@OqYOhF-KGU<4)Uu1Wbz8VL61C@SgYZr1@4GBay?C@-9vUqhlTXa z9Yb(1EL@Z4Q$~hrbH<;ih!Jp<9$6W<+8TtNFcF@t=NBMDea0hD-jz0>&75~_^q=n{ zbo!P2YQ}MUx8`F5+g6)O4%HZd^;{keBCLd;&}db@HxX?veah~!yZ^Qo?jnt9usH4a ztot5`C+>=F?DvvADW?ZJX7^0iyrMm|peHd4d5)jtTD7;0?mwvg!nO>w1H7Z8)f1p! zfP&r?=Zo9iK`a<q>B~DUI*5IIvbN@suRJA~N5>5VaQDhOQcwFqgC8|vBd$P^GWK1L z-`=Ry6`dCoZ6N5h84LdRwK10@`Pejhaqy_pXYC5VUyxsNbNe4YsGrrh|IVQwbqlg| zycP$n>9wdFMYrR-zOt#z0CtY%7hH!~{-Ozo^?G55(~1HR0e{YGxXAgsEU@jEt(zAz z(?-%*amWzi?wff18fV@lJsEseSzbT<5fi2tw;KtKui<lBXhP9^0LRdKY@HQ}PrcpK zjW_-c)3P*)#?p%{k~rE#8aEs6t^bY$wFHLCR<qw!8sCNBmS{7FSIHNdM8}ZaEdg*= zF(10LcWeN9fS)O3xDkV~93ilwog>vchmk)3`llKjg6EA8;S&MM;Rv0xajz(xCl|Xx z#BMQ0wl5}vM@md8k+8y}&{a=-*B-%+FH4%LiK9~0IZpx7HH_swT}^nHpgK)7J1?kJ zM3=vybU~hYr#)A0N|demh==Q28Lq~Pl)B*-z4RGsvm9u8DJ9bdp0qv&p{$ejux4DK zXU=?4EppSRD-IUE+BS^Wfw~~~Jf(l35WyE#!K-7xPrFYRD6t?>eEMY~NY^Y-K>l^C z*Nn*cGeL?fc#b>4j!9f7SL`J^<p~ZnL>qV2gw`yz_)NR|>#nZNZl?}V=qoZL%nfbk zVr9spM;V}@`uotpS1Kq!cvczTA94=0jR~@8Ds`G&Z;l8li04?jr#?L1zec6_fBqMI zd_Yn*u)}^xx9nfyhOPn`?go>WG7bVmG<<{&{u#yr<>)UCTly&FzNdR&9Kq`0&;mj! z1;X9S1X|Gw#`+KopMZUgdxmK97FoSlw!P3|s$vme2qFa&J0Q}jGLOvtkF#W*^9G$y zu%{HLqe%`M34e^}v!X-;xi}as*Py)A)z)kk@Rn3@=cf;#&%5PkiQ2g|$48Y3h6Qm) ze5e#+KdXGmU%{G2DvBSR4HFyf%tiacSbZ=Kyr`_S1HxkUXc%1N86;g&7h28R!1WfG z$!=W7cWcv=!LK!S&2ZvRg&=cE{B5}-P;mryC0?M8HnQwazTh)Xqagh$TzVOY{7orM zVl&9`tP7T1HHgw4RD$l5DS4uH7Oknqf%$J<qXz;KU`it+u*tjF^2Y<>b2_q!K7d(k zR<er(Vkm1GqTLn0^7+)%<E|tQg?#1KzA!jNP6@*68!SBP7}OSa?5<b=ID@DWf-Rd9 zd|ZC7ybRB<Sg63D3a$4R&V@Jv&s<xQ#U$mO@tGezoB;r@XR(8xTsqMmQ+CsP$97y( zG&$p7@#^mS-?l?abpFs+QtYk!iXi)&QG}0;m8=#5s~IS&G70i;S!jTfd;UcKS&8~+ zL9>U4hk!l2o2gSy1Q`&FZ)2{nO3gs<Ds2e`GcD&#EqxV-q8nZADj7!;M)TSO=st@( z=iwhSGOMN0MPvehVZ);IPcuFz#*q)Z91PzZho`|bk2d%_5<p-02XEyFzcqvmQfl6Y z3(7{>KoTgvUDIk{7&7afG3gTr!%_EG=E1sSFbFyj)N>YyldU<NZ#+6yXlGQ62CYOd zG!Yy+^0e5vM?qMVRI3DS2QawD2o02}hW1svnmmoRDu>=Uzdttxe7<5KdT@!SZay9$ zSX?44RFg?s^JE1<`l_B*r!Bds!GLDdWtRa@iF4y=o3~1-<x=ogH&^q>?JRFvgjQdd zpjGJ-DT4<knT>rMza$t-x(#I$?mEiD4*(2vA}fgId~s!e<<pn?|8!lM_bo7Obf+Ip z(Kr}yqDU2|fDn^YBG^;w4HRZC%k+Ra@s6*vaq0zEfGh-Bdn8r*Cp$MxHn12Cu$>dq za21ctP6AkH@_M>V<*|MH5q)Sk*y#JQad}2#1ON@K-UHs!P5EZ6XM8e=aNP=+8$t@R zPAqeT?ErA0__oCBg?P(WP7o@s1p!IBJKS9}$d;K!gV_K@WMMi`#C#Y9r_K@46wAS( zw?b!Shd~`1TS)a!ir;Vwmi{C}z<kBp;i=G^FkVK!5BGvmZc+_9tvfk7M-j4%?Tr#s zD{9TGl6G<k({xF5Rrg}JK*R=xjPF0RNT}*;whT3&t-u7CFZMZMy<EQyfom7pk47?U zE71*X+Nz&~?r6o1)d;mKv4TS<h7nF#3N@JLqtF$|a(()NUa<E24R~fOY2He@_Y9!o zC>*PN(tCAd1+lV7z?FR~1CJPdG>Gm2SVI!SOlQ#dLqz`R;<R@I8)Fj9XfFENcZ6bO zOUjzKQn6CAE<UzeGriqjEI<v_&Knk+Sd)iasZ>9s<n8W<EgE)F$xvCi_p-8BXev1& zeVjfjl|mm`5oWz2{*P4561EU@s5=LutHLp#Se6uB5njqZLD}AucP1$N+eLIo@eV|# zi?V$#+<TF4a;)##2pJh(x2NsTA{k?k%QkS=#^`V{YBt07<To_pL)Sg5^!FnS?sS=M zZHm^h$o~iRp|=+C?}49`b;IL<f}Lc8ShLvI;lyCE(s=_Ah<J)YSr9U~{$Ph4dR?|O z6`mGc1%Uudrtkiy<rjY)Y(>p*DX0j~?C7tz6Ldo6mBGvW_wJ&^>uPpns5rt23z^R& z!SN1s|1GItf$A3W$>_G~tK%cYkyDAB-P7W{z;LDOS{aiL5ip2Zq%Hi^kDTNWYTFOT z$K#hPvU+hZfS8I#Cyp{57L-RF8WsH7H}4yR&@4hm7qVlE?VR*6vEbm9zPl7QRy&`5 zglsi79XW@JHjWtn^(I_hQ}(6<DMGmY;_$vOEQtTKh-eit4oTnqZrTso`XkNZ2IDy! z(2oN&y4SiPRo))9*sFGI$R@W35{#*{WsIn`^YI#vu+qi!9C`_T{aj@w_SWzY@=zI# zW4x2dt1pW;WJnEX*tSHvCgwc~{z9;(I-ib{fs?l8z?=?Rg+-1DnY^3ns_UGY_<Tuz ztqgbW|Lm_U)Bi37h#sqUsI^u_ysx6*oHcG63q2X3Iuycsj%T?QhzOQ}q0VETaD?Th z89_7-?~b3WD&=9yQX-p4L!^vDM5p#GXT2FCY8D4=Kp8KD34{25!TcObmc08aI2jtw zT=#C76lMoBh&qd)ZU8s>CsH#k2mk=q;Kb)e+1BPczk$n8F&uF2ALeC58l$ARl>yuz z2RZcl1l370%M{}Uz`#C5W<Uq4`Ik)s4rzZhwl751zm{z3;M6;^q`@Q+GyB+*&5wFa zg)@w7WG<=0!n(nf#ZUx7ykSEGpWR)?#0}p~E_Iou8vE~iRC~w6lv|mm3ca5)tQ)pC zTb_^P@g7$neTD`yd@4{)Y3Vex3GdRO)~(<>iVQ%%y~1ECP4dVp#7>65wB8c7+vk~N zdrs8g;X?*4*uz6g%4CnKkeF1`x$dFGa$GXIpvjy$KK-SH@&x5N%W^(&W+R*QAc)EH zmkt7x(8^#w94w}LjusJ~`g)C&Qs<(Ksg8U+hx&V-m{YX=ux!Y1CDaavrXKEhnDYbr zm+C$I4pRlxz^^`p)qHYS2TGz0jS?SytTnBv0jDGLSWIOhv`NnMSs=~tuw4;`Ti-06 z;A_FdgUwoFJPf`=9dx5uxV6710P6=NKz#SK%{0;leMw%@-a5cuW8Mjgv?!h6Cap?Q zrZ0Kh{J!S5rlChcQ#hRFMm^h;{m2^K-|I8-B6QqD;NWt)6N?y819sodcS4&f+(-mn z7`WbAV@krSJd{|Kj6JBYRu8ut_7#XAvND%_^lT*{-j)VB?2nGE(^z0Y(g{#*_GmeN ztq)o%X<k4=#RQznfHV2~4u1Pnug_GS+qtdx8r1=C7q&_C@EAI&4N5mDZ+>5&xwYiG z+L08cdQ$n%%DG+7p53m~wJkYQO$Ba*&c7D@@}3fwD!*_jFX6yEYXnSw@_w$mM+_zQ z+7Mm(Fl{0{!42+s7_Ft0EjWGJ(^~_Gs<H7`?-c6YQ<Pxu9WRj6Gk9#tb^Lyb{M+7B zEtE(6Esq#G<h3I6Izvt2q<$BPDTBd1rS)QoiP6b+2{Kim<PC|lro?qz)pJu5s;$3s zxa+P?SHfH&6V}|IU=s&jmR(Q8f|7^n^1NXFs8B1j`qWzct->A1jpU2kU}|=s{blcH z3gRXN25RQ@E3y~STDck$S6i{^ds`I)*2dnX@K!*Vxe#lojy~!K8#lrDaZ_U^6hQG6 z{w15P%MlCr;x&hYE@xdqbo(>P7*BI@cxD?Q6~eg~C-(Mv8KYUoBsfKpcCD478p%L5 z%K;d--*mEKm(D(T9w9=v{hvXc{>`j-Ef|a8X0$0$#GgVO*>%YiI4mA9TWt6(D-#7b zeXMzY)*O(&&#>Z=Q-^ABDI<=gs2fgBImu9MfYX2{VYp%{*z6<qzZ1)G_Bji%k@Sd~ z?FN8u=<CF?d3;4R<Q+7nTw-0%^o#-ozHAy;Mn!Ys#d6}4Z3K`;E>scrlbP4a=WX<D zxrKLhzR?)o;%CeKv-zO13Nric@gQ@&9eJ*ps`pnmG9NSz0F?sPPjlep+PVQF;bMF@ zDyk2${pk!O87a8wR9j)ZX|v(yyu=s*#pG7UL|w*ybzlpUZn$S@pR{w6K4|nj$;k6v ze_i6j@&-%;!@D4i@_p9u<ZFY$&_N9!^a~G8eOCiBfUauX43_&*<E_>R!u>9jf>j!2 zZli$a;uA&_tEa<j0x1GG4;zdr!Jqj4{G|<v67P2$s2X}`+i$dhFZo>ahRQHn<7(no zulO7ui)Ec@NtAh0PfTlf_)JX%xc(De_NC&c)qwghw%9(!Xex@WOr&k%mAF$|AetTu zV4H*!^?_|Xs577<7iPvl`tSp0x<84KObPJ9+h}qV+u?ztMm@zySN9awwq!~|n!NNm zueJKul^gS4(beh=na&C3XS%zS%zc{)5c&AvcB}iem`QV_xaVJP1N=W8gvV}!i2)Z| zN4RZ>{Dl|_H4zVJH^R7N1EP+fs*HJrOasl+(zwu&*O_5<DE;ApQ$uJ9F=`2}c#{z% z8j`9``Kt>2tIhp+Os5j#EYJ2zsV;?q{{@O6C{bTQkQQ=30%kxPN3cLoZ@IM{B2U() zviJk6h6l^DJfl_Wz%et#94?`8Aj@V^#uofdZ<SA2$NKE%%X9?<`7n|?p+0Huir5T- zC=@eQu&)Kh`KQe+OXpg!dROY=1=E~HV~$nYEYEcfS@p2`u-7axxqjBlpH~^4eX~__ zQHF20j*d_spHBJ-vQ4`05HoVwlmGSb#vb^&KRVgj=?6Wf*#qw?6Ce&=npG37E@{K7 ztmcyhP@Aqn`GT;`g|-Ua$SS|7oEx3jBON9EhBX4PHCTb4;P(eCn1XS%W!OV><9GhU z5Vs#8&%mNPT_sC(kY@tA1^eta1%~*m|CtLt757pJSIG%iUx`M*3ti6tk;I1^$JdMm z>!UsASR6KdglJjetzWp{XdP_@{H7yS`a{bP=yn;PUT`7MbDW{{e<_?GyM%d(*AqgG zcgtt#(H^TTh^E(xYYa$)OEz8XSwO-2#JL!hJ1qP?s9vQ5tkT<36GQgQq+uAHV2WTw z<Y_1cSyxHl1{?c)Qu!wBZd3bO&QAzCA#hecv>)zjXX;&2{@RC2#BK337s;|WL+7zU zJw^FIbP>3;6BO04@zJ2P7~2sh!|jp^`D)jcI|#n?yr|Rf2!uWH@coQoBZ(!P@?WR& zwWaP`TrSKt&G2txqWlcVIA)!wZyMf_ZA0a9`R3UUHu)-+3bJo1LR}vmv{AQ~It|m} zA-Wviq8(`Xdb$7uY%GxffVUDmY?O{ZIb-lzf}M#03a|Q15P4D*(^RE)RkfI-PJr-l zBo)yi!Xg7Oo;W$Q(!LpZ{T*a%f-A;DvSd?}LD24Tq`nt@E39PKp&1RJl9CI8a9FEX zO9Bn{Y?Y%Khxv;+`zm{_v|0OB?t|$P=<VqtR7L$(P${9x|Jk6+ibF``KdBACbsZDx zOQ4aY6kF&7vS#g>sMLg16zSa-@jwrXt3)A@weEe6UiEEDwNjIn<XWryVX6*VF;DSh zwOpP&O1z+klJGjHFcCA4GhEzDNU>DccC*>JH8-SvMa%&cT_k5UM6(&=<$1-Gad5r* z|5{GE>jTk_XDx~8PiuZHznC=M0kE`-J1%vl0mq3?*_t=$$)p4WIg@kl(00opExakB zJQ!Mq&ZF;|c06Ft$}64{r=GQrOJQVK9tD+7u8pg?eBl><GKGip9cvE?UpvA#dy!a4 zW$pUIv_ysJd|$Y@;*<6EqaXnVYgZgv;N@L~0Q?mS)xTo=<8vWV<`~cEX4V?G%`A6V z>?4L{IAVkFseb1P$L2Q>DjBrfRUQyhWqbpO)k;Xnus%an$4CmxOjwW*8efHb!5$mp zx#3?2*6DFw^Si$AScP2aO|cSUyUqu$gu>B;yl!#eg*t`vcUpbOxRj(sm6|0?-&;&t z_~Mt3U>?dGe(1#CVTMK%b}kUhQ2+U2*!Vel#NC0$=-+ha)M~u%K07e_r1O>QTvwc- z*+PW()T~+^`trsr`H>Z!BFP}pr4Ef`{f+_i(dY{rR*e!V4aSQ)?SiPg<#_m#+hcac zUDM~XXqb;?(`OhTDtS_V9(R=U36f-c$y5I^l5wrlr5fUqZ;aTyR-(>zzSg*LR7pDi zWrTcJTu>R~za9qwIfIGdEGE3;<z)42@V_zbQUZBi1Tnb8t3><*rJ^M4CW?{vkN#MB zL1h<um3*yw$GW}E!j5=!n(ZV=900J=wI}LysbgxgNNg%?oo~7Wz{*}n1F7$f6^;ku zRW%niJYI$YNnd4f-TZbNdl7;lkWN`m39+`?aD^~6b7g!qlonS}tlL(wU1I|V2F8^c z9r3{lPCcK`xV1CLpL{frakasDjnnY~4X5M|!())Zh}?n8k)Zp@!zmA9q;5&JzMh=7 zSzAArz?Vi#Evf3*gXyPq@Fg^9=l-_sBRny709%SQ1*LE{#To`T(Vd#g-*CcBd1i(u zZH-8U@s<QAc9ZDEAz7zaaYlK8j*&#B@|DEcWx&nrK3o>C8U;NMxQ~jKxy)r3tEXG` ze!Mm`-RI11grW!WyLJXc*%<q5VX;vyeBQbsxg;0Aku^vb!PEZ4&#;?uzS?Y0m=kCO z+c3l23B<$0bu$UN=Kc#%(`cg){($t3lD8OLqHGtanjlwg`N!PbMs~xfbkIs9am=Gh z2zE|V25BFBDE^*#CK5)i5P6X+8#@NsJ4DD~Nyuqm!Kl_pUj!>HXz1hJT#38-rxT@J z3ennLyb26cm&kQytR(;$&9n(~GDM1jg@0$0RC$P{fNyw0Rv8a3(hjv%8XE!BlJxCy zMmgHg(WZgabalezpeGWcMmugnI(&*c5y=w((#7KGeBmG1L48QI=i25d#~kY1f_a9B zt$+D)dREGPDfbyKD9DnCEG-^{5jvut^j+|b_@w<suw<EMf&o;_0FXVZ(XQo%lw?13 zd2=m(@F^5xVC8@#Y?C3E4~+R`*u-PLLq?xhp`~n~{vn0H%5tlr5tWcZ?zb{H#k`>o zK-?CA>Hh{~OOEDjzmWGAJB)wKk1I)GY;7y@R}SkhgT%~&>vr^Bo;Gaia<iq=7-$Dg z!zS*8A8MgHL}240Mojgmj|a_R`w%1UW=|w+chW8-@Zy<g@ZaiWnsZPDyr&JS)>kI# zFNz1pYB|C`^33}zp4q%QPb@34B}X8nZK7L{%7?z>o$UGtS@<f{JT3-{XadUwaXNNn zqkDpTKjO9=_uvx_=dAo#vzdS=u`-susn4Dd^Jbz$-k3-^i!~Iy7BX>o6Gzuf>ljgO zqL#tPQ04wE@y3`(biOw8%W&B!q_Fo3)BqRrvtqkL&L|I_%^gJCumQzHh@>Yr33VNb z?p2H!VNL8qhqE6enIHn-jMfPb1M`RIA9OWkTxtZ_!XqoHL`zfe;(UE*1%**oZwsa> zzOCnCJSVt^-T|EO0s{Exn-%F^HRPziB)tk>(?&Gsm40t#Di7zp?cVH!fK|RG19Yc< z<>Vv}l`%`T&)gA3vo**j97+50(6SD9e={<FjMuVb%ChL^l8jS2BFd?zOecF|#Enap za!C7~H-sl3jvJ*|QQTk%Fj5usSFNiUBT2#Z2FPy;l)5O)S7z}ysE*K=9S$As^J-Wf zfj)7fdJ%vMaesM>ud)Y9<a<U3fmewafT3OMy&dIlbFq~ijLzQ!RjSQMd#;M5G|ObY z?tN^Z*RNxyOn(OwnRwCdaT*?c==V<uGLonJ79aB}O<)3@N(T|er$jOB*+65OOLZ4F zYVg_(#%mPzP!&Kq8akJFi{bE**IAk9H0qqFUx3<0y9-|>JTIVBm;o&kiWj1ufOdQl zsYVm#A5EU$UK^mSh@zTz!*K<U*W=B@Pkw(dZvx=NCc**yB7reS_i=bd`n<JWwrd2K ze~f(xLqK%{ApWO%<`r6j*9VA$rr75W;8;Yc)l^Z*9yaaUR%Gm#q+0}#QA7upez$b& z9{G@tkXkAH)TBeCyIYAQQm+S5bwU@)on4;^L&Do=^B!BpN5e>S6I%o@PVck47IW_N z!(cfTFZUk>8YVF#RH}G9MPS1sO%U=drq!owyksVlH(I2t$Pfd(Uk`Z*U$QpRkKe$M zqtygu-2`v=F31+WBIPb+O!$xrMB>>|#_dNd4tB7rhMWU_sJuI>A!BAo8wEhy$OtT1 z+dHxKR3iO|5aE-?*WMw^QDmnO{cNL6KdV;sU-1xSSaWAEM@U+}*tFk|`xL()X5joQ zD7VlFE|Sz}b%sBGC1K%9BVJ?q`Q64=kCGAF4Nam(wsoh`+wmgV1$dk>-qU5{?h<P? z25Hlznr$?H*_yA4-uyr|g<E?nut*Q|o?zvFifKa;;sU^9tpPiU=96M6YxXWA1dDy2 z5qT<0N9gO~Gi{edwcsq}#-I!kvJRf}1-cQ-nqLb#6OagjZO=g^FBE3qs-70OMR@3n zD*BP&@`Nq1ivta{w-^ost-!=%9mR5W-lC5;903J9yz&v^Esb)PZe<Gw<(=qfK7aEG zI=)oSf5l$#7rO+A@KO<~w(6vO%K0mYZz)xA*RX(8{1<(EeJ5@KW(2<!5uVu?7;85f zA{U`(SB?dUT^X0kp!;b?s+caF#86-{Cu-9Z-&*knw#Jg#Dr}V!2I+@p<C0KK{;>cZ zb;JoU#-fs_dod6m`SW#6kNw71K&pTn3}4`Vbf!jwg9H-g$V(8ey0D0R_B#pYDuxsA zv;96t>GF)IFa^CrVVv$NP;YHsfo%_H4!XipANOZK>==6m-VPTT&eMjy&1Ma=k=`o< zNgNJHrlJyLj`w(yK;kHI8#Xk=nDUMX?!z8LskNzQUq5HL0&ns~xSkYCm|hd$Av?v= zdU!F9L^fjA`0LEc)A}zOM^hSDto4F06(<2dj33j~08;C;(WniFRA~E@_ex`E`*uzz z8qS(k2;M!J)omfQ*`P1e)a2+QhVVA#>Hr_-89_s@X(kgTJY@)gJ}x8+2kJ@HIRw2K zKCPJ%nH6WG7GM9Rtsq;9Hi^y@);>E4t!T0z<o6Le*oqO?i$T6UWym@@({$B-qovQ^ zhU{yde&V8<CdCe&SEf#L5+RDf&OZ3-**OvBZ$vPa3#3q--=icsZ~v8lz=xZ?O-sEL z`%aZMxaw-@-dZ`5994IeB$ECpZvqrIq8#$KBTke;?XhqWB9>{JZ);vp23BolLX_J5 zd1)X@-pmAvH|C|z%bLHR2k8|D=rB=oF>gXhQU7iOl)C=pIwDj{1h9FmTF3l+t__p2 zWs1S|yEmfCL3d?WKz9(uP<5*%#_%W*4MPqW1hD|p*-#S0Vxe_P4>=OvF200kNThwV z$fV)LHY=Pg9PSdq@%Lv<-@2yJN&6>$NX(+iH2}~MIGFEnmb#o1l*+kL2xg9r%?Z%4 zNNwWPRrU*|m~ByOC66l@ZpC)ciSjj7i&FSGz5JLMkNp<A#aqFhl}`T;<tu8M>b5Hc z>fZs1zB4<IF?Q_ZTF3o_uZDJkvBs>iC#GRd(Ipn6cprw)GglTNmV`gRT0YS2RDc1U z)$TGbv`B<_U1ZwuGG`+j)WvdAY@Pw}-rR{6#KGghB2G$_O??g)A~3HW3$PJ(PJ>iE z9ZlJPRyVHTg)yXPAV;o!mzl$S%b<M#*dC!f>#t7TtJ*OT`PmJA5laRsBSk52%e{M! z<Ghu+OY}m4TRg_}!c-AyIf%QdpR9AXb@E)Dehu6_Z!hqioRR?y1{0Oj{x*lc!!hZ| z8j|0*-*^?TqquBSn2k`E)x&|z(0P#op-$6pja@dc3waL3#n+4e9ToZ=2kXa%;}P1# zuBCA`dH_dhBY#$*omlJ>_|YqD7wO0ZRK~ml$)6Os9L59-zH%5y)!7BGh@vA}9l0KB zDeJZA<@6JB#h1=aT<o7eUiDZ~tO$Ty@wpWQkK<x))f`~%epjb0m{t|E#-{T>KN<>- z)F-ew+4a?H4mF<*qupo;gqZ*s(DxK%;`Szx>Y$NR=`cZb?@NA7Yf;L@C#$+obM}Zx zMVxHCO5%u=w@Hky;YIi*tz8E3v>t;Ui%LeqC-Z#Te%T~WVRQ?DeZ_)sU@<8a0xEBD zT=gtY=jHIJZKLPO#Tf+5tdovY=Xdl%1uQ)UmNWj+p2d>?())4HNKMALoCd4{cQM*E zgT=w(ZTt|723HPi)<}gnogwl}+*AW2Gr6XA{eb)>E(jXe)>FP>?Es!MTQ>%=5Vlf1 zd)u2Jbt@4{WzfLsNJG2qWRdzI@I@jax5lMDl61vw5c{Zl8;WGO*E?V>iUrkM0Ax2H z`A`Nt+aux%>lX%Y1Z)>f#YL;v+Xu_2<6EfCj~ipHU#)id=8ujx=epc^(~$oskWd-B zvyqLz*2sZ7>CK<rRFu6AB2Qb;{GZ~x$eb<0N~vW+IBHT><&hkLOL*5S*yso#u%syK zGaSTxq}taDEDR==EGD`P{l&VNgmUflHb-}nqZ$mLUa<babPf;fym$_FiP)+_H;0AT zc}#8#N&yrD){KewtF4H>5S8u$kzXFbZ6$z-G~OUo3v_6EMNb9WaHuh7@b-P_l@kJh zB+lwX0K}=PMlMv8vL@yD5v^kNY#7f##CfN|hxO8$R>T+t!z>qo1(Bdz{h*WMgCAGv zosT(o7SvpGY4n&QX*6HeYYgoN{GgS^ZYicI4vGcVa6m`CNkyIrS|9Ye16%)QyO@ND zmp|6=93$20O)T?9(g#uwK2aHNRFCtn%47<5k0;6mU=mx89F5%tY|dM-!1;`1xA@_T zC7)mb*5Zz8(X9A|QW6pt=>i0^E$|PM*Ko)7=m890W|6(Q$Dz+f_oxY)x&mtk1YXvC zGz=0K`?+RLCv$5sExl*o+#MBtI%LYpGE*5mbI;Gfv|E7{3ub2BF2rY?+@_uC;aFh? z$Zx<h(FY#B!|_jyr*{oEeMa)@!BPJr`Ti$D56LjD*{NtN)q8gDBaP(GbC%ODo!nvv zoeiqH0_cGc-gF9__9xyIR}#2g70r8s*2g+Oh7xBQLj+=JeqoXlV*bSuf7XWRDlD&x zAvv#koiGu`mFm(G=l0}=fGTiX*GTOXC_u2f{%>#(xXKdnnjKT_i{^kYjV)182YI%D znLQ3ecY=Usi{w*p3Kju456S6Ft>rNl=H5BfQjv;Eo^Qei+3JT=6tTH>rK`H;I4u}< z?wIw_ACDoX&OfzNYHGPXtgkE`O=RKbvbp^?D5c%nDP3$9ZGNPjP>)7N`wR+=KB5BU z2*2-8X77n=PG6UsQ6a>W^`d~-x3D|wtd%)8VtW(Y_=KaVf;-5(C-De!I2KU{dh!#< zs*3Ii69K(yZ;ivaW2;o1n=0bQ+U|cdDux_V3Kl4I1@Udz)aMLChoB5QamnK5aVE6Z zNBUX)5x170Xl#r3lDrtYcW3{68Yf+xYf?+!W0ZcOqvI1Z{vbJ2O@c<lF%3ln7I0U{ z{Xfpk8YHik)dl=2%uutNMgNO^=pYE)sxgX>yT=mZdk}b^E#r^`*60(zKUSsjg3dhC zD~-3Hb5{H^Pa5xWN{Q+kIYxfdUx(I4*$W7}B{}FP-sNc>AnfYr{hYhU)ai<!!lz!g zoMrJ2FGGHz<emksS$}gf{#X45cYF=fpxngk(Mwp9{VjPXJTmr;%9KNP+-}U_4eMXm znKo8!5&Z~C6P+Rr6ejUmxXc0761hU)1VR<W^IiFsmZ4ne%Dl9N{9L@!8(z>own!XN zI<x#6XC#vU9nrW6@>K>=(c%+FG>E?R&b;mLb*Pi*Vp~UjSqBEg>IExo6wR0T{IXkj zPZiiMSV3BkJG>c*eoSJ3tKQ+pndKCapv_wtT3sc2UPp^sVnWXV<e>74Q{QI=7yEE~ zrcpfzJ5LT9KqfL3XrJOU2M|&9@KP#pS4l;;;%1jKPJM2X=~!nG!wx0O#aSx9uFJ;y z8z%viu>J-Du*H~2Flkl+RtodkC-l)G-OlN#n-m-&nl44!?sG99ONO@R&75y1kPrvd z>8lH6NQ5F6^6{WEv;`0j1VEZDWCxJ6Nt07>QN;CWlw($n(Ko~@Gb4g`@!t%#7hqsQ zSt7<gPp?&4yq7bBb1YB9w@hdq?08m@XdA7+CTHtmb?)O4he_GXGh`5?>aqX-obO;| zvYE$y{HHsY*_PxP3ZSCYzzwzzDGb$A1q8&k%uVZc_C!%Vu09wSUYJCi-+DKwVU>;s z=#(J+ZwY!9OQmT0_ckG^n*s5j|3{nxo9Mp}O1nA4zrqR;XvbGu4LTdNWw&w$<#Wk= zRjwgI<tSeCIO0VJ09T<gToTVv@?}r;qWz8DGpQ7}|6q<sBVf9b6>}R!gp$yE$9woT z6^-p^#3P?_)6#c(M>kZ|%nW=7VlpO4TN(oO>Vri5kEg@PV!-avknK-r`m`MCSNlDD z@(<W3y$kKQSKGDTEm8o=+GZXo`{xIB$On3gl7aVNxw?XxnR8=|8<Z@PvRUiPEt&$R z@#cS)W>QLHD~vJgYIR0&aHR+U4HQgBqbRV+jGn&umej!x;P48Ut`A@i*DcmImfq1~ z&UL!F=@E3qibgekE?*U}tia)=Z!I{R>a8=f8O-7ZPq{k=CN(X+Q8$Wb4Nu||gU3^< zZpI42vV!yf0C5XDb-tplvY#0L;*@JNamQry0y5r{)1Bk`#p1DBV<B~nOS4JRs^!*~ z$UTW|xt9|u+m^$hGe^u$EDeFH6QSm=XW>^<!Su#k9{=cZ@I6UEe1g~T>0M>sd06c} zh^Yzr!tFvXNr*CH?(<POsl>mN0<D{4b@wl=WG~}wH`kQF*BFcZ)=YHryqEm~XAKAX z`PQ4%=l^UUX?*U?xnXr#&!{m$2iSArD9L~jMoxm|PBh&a7I5L4HMviau(A<3Aco)b zWK$$1I~1hQbonC&+Q|^55n*A-N~rtL{B`Y$a0%1Iu8qHXM6IfOAYcID&vipaf1SAX zHrQ(nY9eYtSyFKr3ra*;O4gC0XkI?4npPWOv243CrW*SHVw45=ENfE03&=ugd|LzL zvkh-Q7|TSmF-C<?$uOGIylg4W{^Mw@AyYFHaW7&5uHFF{e`NK02?D1k`SW(jP=YNZ z18)i9Y)}VCm_>^LrGDd`w0?U;ItrB)aBW$YvJ?-vD**N0>%@Wa4vN=>Iy607d!c0w zP+NFLusubc!Vc3+>iNx=#n0TymOSEk>7xulSW7wF4uHh6n_yR=>MyV}lyzMP5I#BI zIHS*T6a&uAAqGLzg}zH<KHc6ho_Go4qaP7_tqu(wbAO=MV!;a$n~JdQNo*Y~{x{c= zNl-ouhLB*Weyp;;s-hQqeBlb0E5!qxGDF-uWIYV<R$Xn+47kFxBYQ6zw6tC@-`pcE zfTweAMVhVwR3`BN1o{m7E~p08^R~CLz1Z|GxEPr^w3*%s?rv&owi0a_YxasRa0IlQ zp$95hi^yKPU0V}R?RE~1K5sW8RAo}VTEjR7wMaXy$Gks;YEWaTT8Yy#s;01BMK?Ts zA<jTq!$$+SOCTRmf4aE4L}fCrc-m4oB3#+M@~QBPgpf4F%ZGoIwsib#HS!#nV^u8A zkgOQFxg!V{Dn}-J<-rMO9p?VztW2Tmi_Cfm%3J(@7omJ{tx5SUeCXkJ|H@;KI+hCy z#glSe$5u0#0db5iC2CAMGEp1#=*!JuI}G@I=I#3g4fFPD^i%{eCbs%6y50=SEj^AH zXcqxSalDj2jl|5Gy{i3B0bO5n;8sxr;2CJpm0~DCjHNu?LvZxp7(N3YQq|_~3YvRv zY=6KvdUJI-k=dC`OMwy0bGy7x?e75XEO1AEz8r9cP6ce4ogNFu&3HU8&a~0s@z+6r z0fy=EB63Vn2MBjLib>SLcx;Q7=E7yU!hJwxd0m_23Fi*8&g9yJL|P0H%9pQ&oI8V{ zIAF%(--k^mt;&k@<5p>eXBhrVRD~a4rY8}$CJq>L@vcAZ^HCW_rSbED@_em^1Y^t$ z`DeZ;?cVClNuHVRwSI>CPPAj?V#-thB&G-EQJ+{zY()-l<?~-lS5nW6y_UI5QoBmU zI2Ep6Siv?`E=}!WMWye-TVRpnD3%gN$H+G@44Fr}`$J1Zwe)bId3&kNCQo$?1a$+` z6$}`pN@Bi#s73yaW+EC(6j&0}dD-AxnGu@l<Y`uC(|tBQ=B~Yt1oWTv{#)EvSbKP) zIu$mD&s6Uo5K@k9=FAW#u|ZDrv7n(#@sTkGw4y-32oVM(K{`LzY^3yVOPcJ~z>K>6 zsLi>oyPi4_1xgB$>Y)i<k$G8^7Yg&y7Ek;~9SfL-SVGH0Vj3KMo1gA>65)zJPm)CC zIi;PW)(iuMa$V$=J;UE8onU`N1+i5FUP?Sjp$Y>KC85+NraVdGziT^Q;kth`l{u1w zuekvdmxMItc6cGkolg2+x!>X$EB!Qllz%IX^8MyHa2IkfW4OTQz@AE0;W8tYhzS7* zRg_)N`57!&$%@~F^9}Si{G@OPAQYW@{{f)hvc&w#73L(M8pS=rCzAO*73mS%$lz13 z*&nIvd<-DrHk3BNt$%ujD?^GM{iKQcs#!#1K-gu3dHu{CG%4rx`41N<SFAAwaWACt z?ApKI1t>=xM?r;Lvvc{52^U#_29VAT^z8w1jOmR)T{~f04hj{8lLQ$d6#d4ivrt_o zMOlL))lt+ITqe*>3y1z0o6k#yU3Dhg5$rCX!;NLbS5yR<a;KX$kWg9CNP4Qdume0` zuSkJ%tX$Igav1&6-Tw%>_Z0AIVI{Nq^!?n8suCB#WsW1vNt1j67YFk1e5pGM;BCbR zHt$uh=>k59iz!#4Au>$|D>A@cd;x=M>@*LQX2~Bp0f8DkFr}rgwm~$~-Ih9L->do; z<3U8uJz26UWQX@j2#;`ZIa$99zW@`mnkdw|hPsDaPdluYb`B6Riiso(E6v~_smK<4 z*b!M&&fwuH&F*{8xfbEF2c`{G>rh>gZ;+?of27`g3!!WZ_Di|ojt;f#S3v)$b!A5f z=JxuaIx7-Cxb#>6cJK@ymLC}<VH*tJD5cWK^)+^E;ie8dSl`AYH;D}2T@~rRZGD2! z+~7RDO|7_Ci8vK|I)C4~5C<kW&xDJnfgvp5eV+)+%gijrq-tc6TdpFqPQPj530W;e zp26t|d7TXS6EJ5<1+EF!WDVll{QbO;__H2=292{ARE3y&49^`ommIE9Ys=8}EMd#` zF|-I++g3+0<U)bFNo?L<>UyGeQTAN;l^O|IH2$&5f3Hlc->S87m6*hINK9|fB#jOb zRk?JwhCP@-T2(g2gw*|j31HMZI>#0arxJK%CvwK32~!4ULM`sl)^EYpC@Berp1N(W z|Feh+nWL?5N_iTG^t|&5SJ;=VDgedH0X=Tq9rj->ITv!f9~ft$^=}N`U}+o_Th_$t z28uzA%Z(XGt*DB>M)tHv4JIKghC)5-=`FvswH!|2rtW}|1s4yca7|CudNfoeU3!QT z_tAIVs;C5F+go#w1k{5m7eTEB@X$~B%nS@|ogp>GU$Gf8?vNHeo2JM+n0YUE4}-jm zV#~SMSjE?KoY!zFD6&_<Z<+B0hg;kvb6WrVz9<!NtDTh`Q{O-*?hS)F-lrJOtv!$_ z+ejn3`!5zgh~J9Jn>dsZp!|$YfL;@j`K|}p%ke{kJG+EX@N9~ZQ}_W%XNe?{<GSJL zO~#qPBBE{hHzVEI^OZ?Y9#};?`7#14|94fAu6Zy~YdK!D&<*0N8c6x_d-z)_VHww* zl}}<DF@fy_3NdOxN<|B57|<EdBAuki6M=j|p)5)msC{YS6Zo<Trf%WE1cW1D7?98> zC*K6~wNO1}kO*7)6if!xfm_Ov`tet~1|-z)Q(huWjkYESWD?ptP#}k%h9bgMnoVpX zmxqhDTYSov>ueK|Y3diNQ&$X*3XA_Q3bNKuWm*`Qe8?HPymIIxL1-jJG`*#pT1L6N zrs~9+-}nUA5f3I+v4^8q6nCGt0$dQhA(D=WB7MTpZhLn_(QhHS+ue{WZ(ptQD{f#{ z0qp@0?3!A~4l<%ew`n<LbF#AA{erVz!{$O&IUKPPkFjOH-vv*GC#^O!==$8dDR>a! zPT5rS5pM<o4(JB~kB&+O0b6EY(<Qz&KzHQT=l+m=sqs>CH-E8X_yWN!|5F7<fY(jP zX;XC=at~aeb?Vw4SKj~#+C+v;hsZ|$+N#0!O&OCphf{rrOlmEOkB%1S<e{*xPBxsX zK986R>CsSO&>%Dy1;s992XczML68BJpAa3o(A@fJks&(`Ua$_y48fQs(Qjh5QIAza ztzQUWVQjNF6h8-BnB|XUqXaMK>0|$|bsSvYcUs@nU^EX&lfLBg)?Qro1gtUctWq`5 zG#;P*iL0h!RC>4`6L$-yHoKM{d4^-v@>wUhiz)&~8H&Xc2y<(}70X4_@(z~)BCB&` zn$3!vKcD3uJ}U^lB^cRO*v#hXJL)|Y$W08dBX8r?04G4$zW}EayIR7|e0T~5DmVMu z3hbH4{O{|t&~ENE=C8_g+vk;HAxQh#E^WIPOOxo&)T}ew4EP$wwQiYDjSET5334On zgb(B-E9b_O2g7Fzqr4Sg{Ri=kkjSV8NsG0aJQnUE&vBh3n#G?65tPP<U!i8{nF;)5 zJZa+)7e3uN)_mDUbbi=3P;O5ZO-3ktDe@J>LMvdtTp?)2{){Q1OzXN>r0W{zx6}UD zvO`;QnDbkMLdP9@g*t5#LI<EzGkRfEBMXa|Q!yN``59n*s+V5-GoRQSB)#o&rVzt+ zFupn1YmZIx?9*BY$dyOuO=t&jshhPYSz#?h5FmhZ06jE&-?L?W7al45zebc0BBKKD zv^Gcm*mAQANS6ce)X+~_%S+M#qR!Xkrq6NL9$T{qWN%qA`UI=b7PL)eCIT6Bz|IwP zjFb}x7r(>VrKfz8^pg+j*pTQyUb{<d{j||6UsZh1F>V1HIxV;zRg4tQfUN}4m<wDE zjzMUkLjSt^I}tBC7#D|`Zx``n)FKMy<-7t4xTEMl(4@6NUxFk4$rE{tKe>$%Xky2i zv&r->umaT?tEvt2f(0>rdV1kzZI7wl1w&>SIQ{l>BjtCP2{G1{6lk6jGgUaQB><LM zLIak!E;ROb)@J_Mcic0N)%=cnN9|F9muvBlNgS%LdymdfpH$)%KBTLWTb?BBjveTb z5>BKZ(QqiGqN$fr`=%kizVm&Gf(IbCsJ=Gi$@A{1T9bDLo}nK61ly~9+$=eH-IN;i zdQdtw7aHd3=S@McyzM~`{Gy^r+9j7)69djSf<t8mar(uGbv+0(B)wa<noFpKSQTz? zwC^_7-T7%`lG%XWr6Ur1$n&;knmY{>-yJ&-i%rZ4BuJ6Cl>{T`UL9i`I9~Yd8KdX5 zN;$U+g*c$PC^1y2EBfvBQFpMO;u4$;B!9)^p1Sa-;au#9qe!z=^N@yvkir;1LX8dv zfvYWZxFk({G2WJQC4A3;G%Z02kBD<5CoD>O|Ey?wc*{&X116nD`zoMmx3S32F+!UX zH07bvo<)}nnL4sGjlAgu7K&2#0cB|IXj|a*Pt=2J`=5N_jnc>dTT>l`4}=vJGo2Lq zB)tP)c=P)<RH=moP&hAC8@xv_i<|;8he~m=x8`a=DiS_@4g_t~fV|K`W$Iskm;boA zb&;r3#ZPesGGgK(y_>5*IFp=1`j#MK539~t>I$HkrF7F%lVbK_g%B0A(>jrbs8^xW z?}WJv%oh3<*@;mT2UV(yv;LsTq7LL0Fx4TheYSwte!&!~EdA9y+KkcVA}SOQmHJW; zNQc?2Dz8}QxLbOJ2ijS};X~VON)Oo<jqgv>c!hK$U!DF<-Q=PU^g>vMprj!L4K?Jh zsOSoys%>KAWDw9vV#1K_z8crLP8BgCeW&??Xi{c;=iY<lb;hguf0F?Xy^2W=6>b%O zYQ>D<JJ`|$L?u5wAR2z0(!ql7f2z-(smOo9k#3OitQ$M?{4@#|n`YhiCNVn{nvFY7 zVYy<m5p!hTiI|VGNb$!~ag_~x<LSXW`?*4m=+1TH&4aoOGhQU4i%-Vfl-7jbQo62+ zaV?~c{sdiGCxYtR>d9?990%wQ?wJUSX}QXoZh6xP7Wgo5bVad1fR*(osGmkhWKBvB z?0HX%4Ns9z_28Jc4f74te52bLR~=V#1qHnls#tg8rzEn0VG%@Oe48bFJGkRMjxHEz z*H8W*b=J=YZTBP=GxA<7ngdP?Ewb4TlFbaWTOJLyr&QdgU`6vV#<ofdV<3|AIzz*V ziJHtR6;A14yQX3iAcGdV<Dj1m9V}Vno#iodVe&rFU>L3OYUu=wmVAU%_H%$~;9f4V z1+5}41w~eI&L>GQ-LTLY+TcHK#Dg2R<ATy1tFu_nz3GM}2x~gpja2j9zu%h0wkNXi z#b@_%f11j)(D~RDyYlH4Uw0&8;(R>~V(lu^h$-aEP&|g7NZxFmR9B_Q&7w`I6O~Kd z*>pyggBWQ7hD}ATwQonG9V|dxSD#7~Gw}nItwLl1!jGjEIs!qk(Jfy~{fca>!uo)f z2F6+uK5=nHfh7&3Dl-#Ugc>Ts+*Q7XUP&nf5B!Dd0eQbDh2_cHQ+qZEQ<i<n;qkf> zuYo_T=+s{VH|Mxv0l&ywVt~X}AO_Y_{(n$b3cU2ORQ?tE?O1KsrAbJsByHLhA_>r- zTDMUxV?UzL-uv@Ic4u{_0?6)tIScBEl=g`5$g`wW$aI}6L$=S6GMFF&p=b|}P>nX* zX^}5l;QPfqd4-^Q5Q+q1ai7M=Zz6pYUQVlz`Y!0CdOWhMz^@Ak(Xf)hMwX)uEMV71 z23fOC3U<w(06B#+a$r%(f`{g`QZWBbV8dS!4m-O}AoD~A_h%Me-M{-!{YMM50|DJa zqCPevMYT>_A`y?EUT=!u<grMPL&At_rbQF2A}bUCx5i6sd51#P&75<xIy0p^GKKEO z=E3<ps;mP0)e4jENq}6Y!#y=EWtf$2fO69pET^tiGHln#BJmOrAOBydYq6&uC<X1C zadW*v1RO;9C4M(pDwTCtR4k|c8~vjXwQ7E`8LV1%h;;V`zrTtn6O~L*Ejk6*g5YoP zp`2?EAAcT{$I$Jw0F7Eg)->1|=`p>C_A_T!cJ_GW!BMf2R|4zE<Pzx1ND)%V_8~A> zZc<j3Kv=lFJ#NyhTlmHl8j?=uWK90wdKiL7(5}vA!~o&>k7c>Vp1Lv4eVDbwRr@RU zwcBo|^WZO|x>bu8u+pV}XMdP`EA=5Sk6!h%C=>9IIVMM#aQg#QYCMBoX)``-_BlIN zJJfdix}p99z=;}0dCzyv)8kbwM$(_{mG%oYv}UReY`-9;%FnyaRNjWv{Q@#tNS$X! zbaasoq2?EckGP3ewsQH1pC@CHrG2r)3BdU81LO&vWE;&;Q1Z24HyLkoa35w*3I)Fr zUG;-u&$Jbo=V+JW<4Ws!T+`dZx1CDx9&zJ>q)n6Wey!sg>|Mf{GE$Uf5%{1PTE&Qa zqH~w|Dm;E&9+dWrDeiq5M!9ts((}>YTd}TAJMxA%6x5k87Ha+(v;YwlwWz;yu}_I1 zVYxYe2~y7BGw7I0xW07skUd8b+-G4G-(GNE9K$QjKikmM#1L{AKC{kauylQDxDSqL zfqL?EHhQA&WFZ3XihrPul+GyP<>6urB-WYE-e&y1^Y9oGK~WqC3+1qN7r*!eK><69 zW+R*}a%_=Mu_z|@VStZhNm!ly9yp@Ty!xQ|UiXO=u*26PRuha{(K+vRHJ>St##KD# zreLIsmcd+RY93sUQWgj*j0i#m@Gk?F*&-Jdr(#~}B^<5%vhp@s&*Vf>Fa3B-jR5y= z+W#e7k@}xfx}W_@?pn4g{|~&w)3X~CdT6X1Dpne_qIJ#+O0?PM7s-H3IL-MO;>d_M zJqM~8Y?hkdvGv?f^CA|54^{UW5^dxX$(i8GF(c9uXiK0R<<sMk5u~_thf_54^bbkK z6d(90qIy%aX8}9v*Y2GXbRxc-pU6fG2nBysO0xBGeyUdHj-}nB_lxD99O(tT(vV*h zU<Pu#@O!^d#@VD8I*{_t2q?WPy=&mxlpg*%OBepqi@bgTnp|Z$(SrinmMIX#t8i%G zT`=8nphgZB4fe2bjXv!oJLwrBQ@|>WPYE!`SzF55WlKRBCT9qdk5U+A(10}KIqaj^ zA`=4(K2DoD5}o^F7KJU{9$~C0N~Fh<1p72}=db1@R8l<8$W}CU^F?O=REtN0Zm5_M zg&li;*t03Z+g4Z&x1>uz1`?e|rMSNTXmLI~cY4gn60E#sY5Z~8Sy1qR{&EEqVy;_? zZsQ=9^+fPX0_{!PP;e+F1-{wvJSMQbWSp-S*_EGvz4I375>dbf&Xcee-O{A)aR109 z8k5zqZ8O>njl>L3+JjGh=&$~N`66cxXR>WqMv|s}75ea0n_d7IloS#(+W_9-uwa2i z+&Bs+v0Rx&CeV*#@^S2r6@MfUc*$EH`a<hCVx$a{lW{H?p(lCIxHQ6j1H69}r|#%1 z=<ih!9Aw5v)$uMT>j$L~z@C8JChgIRKm~=wL%Y-#*z<jF%p<yth0(`u>qnLXdi1EN ztdmeDFVNg8L?dldRwK7Xhqtc{CWJt8mj3exS1{ZxB**GV!SMsWpj<QLai9&;bgL%8 zG8#kSo-L~cTZe`lcBue|bV2RM51^|Bk#~gE+bV<H!`>2N5)s(v-yO7itP@%v?OMDB zKx-@bRzMVI*NhXifIo-+SWlM-xoOI4+l|#f@d7`n`9v7SDq)Z+yUulR_@wC^_TS>B ztpxFf#rKvo@lXu3mh89+sOw=_e_A9!jnqNbBu!^Q*PB7rip~=nEO*4uj5o2Jg*c%R z3HRNYD4V%+O(jPGR}N#u!a9YhP+|EpF+BQCKm-T@?J1f8gk3LHF<RCu>8>RYI4l*< z59}IBhu}jGhdD(AFS?P$L+3iu>Tw`YOPhccuSya{RIK8)(AMO6MfQ)LZqXG2E4<J? zu<ETAJ#ZC5#gKceG&G|r<C`0U7-m>GBj{x$8<@vsQbrWaf!-1B=S8f9S@Ki+4>N%f z2UvDoa<gqaP+3aXw+K`B5fDFa6ywEVE@=i1Zv8grovG3m0b+Jz5+i)y!veeo5JI<c zs|s&>*r=0c&9jK7;{y!_VN44b3+xVUT5)Gk(*POpSI4*qRnbqZY|a7}*vlJ1gy-ln zIOO%Ga2?>h0O3>e6BaJybsEz~Bh|e87Yh%V8N;^#(@I?vSM@9f7ziimOWWE-e5PCK zfA(?nf8oGjtHIMCvmYzu#1PWKDZ|!*YuS<ai@7m3U$Q+JCd716|2L3QIiLiiV26C& zw~Q1T%q$6(1b`+@BZ?)tBJ5cdoYj&wU`Ol2m@xhco2HSEOGf>@LKr6b5uCL{)eJNm z|8lchR`icA>uV0*lt;71tYLNC5*u2htvM+KU(lA;vBwPa+~?OhQDyW2BrJ^eS##+O zKiag;+MsurQ5-<!E#9`}uTv~<pfJ1wqgpw2q;OCud*?K<o;TDW1BV5r#o2k>R<~a& zQ3FU1GM=n^xiAB-by#aorkBVJq$bxjSt`Z(f*vy*m%cyPj<IRWtA;D|@y=p<`Ce%X zkW#!MS$XT|GDx?)<jsT^$t6&L#Pky^I6(LXkhHh<($2Q%tZe~LdXsSx=M3#^X97A- z)<#YqjP7SPQ_#%`;p+if3Iv*bHabpWD23Q}zWAq~rANdu_lRsan*>%i!lMAiCS7+m z?0cTcBBQAf2ca+H^oZDVxe&2Wch3-wuHmz%N(<erVwuu$RIvCKLnKZt+?1*{%k~Ps zW_QvMj{T|z@N2`h^wZN!Ca$b0AI0rAD%lCy7k#-}{wc`4dPm026t$-a!G>m}mQ^MR zYQ+rn{$tc(me?BrD#+y!TnTS&c@(TJpPWiTlb|{?dXr&#O@2e5{6A1)tuu5Fn3K6A z{U`p4=<8+Ob#RwZd&om%d)jeMgQveq!hmnuQD40ZcxS_5ul&nh{Fwd&1<3%s6+KxH zG^8ypEmgG~EtqJ?@bp_mUXtn0z?u43Ugbfl?r!)mjW95hHik|Os$O&+ba1x_$FB&< zfP6tR$VTbDwTK?DQ9#Vrz6uwB3|Pq9w~UEdg7tEup~tii1aWSemu?X;lbeCYdfPwv zT6hMq@%A=Z3@wMA0_hMot1eod&QK*op#y$vzhcf5((HQHY81)8{ecnYXkw=-Va}Z< zC+20@iFNZ@iwgdjs?0+SeW0LxeNZ5^^9iXDTN*td?GVo4t2-B%G?6Ne_4bWsx6d|i z$i$fuDz_BlO?%FB_?eIgP9OrU_?aybE=|Mq+7Nca=Od-F#ru07%E8a@G|D8tX0!A- zyY2wtn=txYu#ag=a#`WCfY+x0>$L4Gbb5L(&X4l9&OfdZdLE?rsf<UF15JnTQ8@!> z87ZHxK#+sW{UuyKlTuRwd0ZjC!dE$~owUI~#@t$?1++lS4T4iF!s)SYD!{<`%}uAO zfr>!GGb@SZ^FDO}lmz|se#U@eCY(Jbw~F+d^gX#s770>ZaW+gx2?_-5_RmshlmbhJ z%8-*`(AQ`H!;pEjGx$7TVaP*qgUQgos7r!13rQ6Ys843eoxMkTakCB-?Rg{s%p*)_ zCUiXub9T}>X)-JaSB${?9f#2xzV+MS)+5dZ9Vxi$qc!%vMSjDdZxo2!!lWq1oI$Y{ z<NJPA3&Pl_qcXsF%1D(#y47pa<z@s=Tv99H1t2!zpD~}it|OECqpO;Xb+{q}%)Ty) zFN<MY14hj?5t%rUAyR_*mqAlp4Ok{rvT{KD4+@g#S8P{KL6P1~tR49faqd{V(C?VO zZ0DTsY}4yisQ%(&GO`(c)|l5vyxS#jjd8)UYfynd&(;cU*?9#B<l=Fb7KE1<HKaey zw1d-eBw`V0VIH&rU|Ly`xlKHelC1(%{d&IFo7z(g3*Jcxr)MRpB`fAxj_nCn7Ayo= z&j-THPZ$sg$!sKMg>yXGts!MK;9>`wtdura>rzJnQ{*bwmtU$j47dQuwPP9y-6svS zrHoA0YutyUSa)iOjfIZGsd@w*PBu}4TIitx?^R%Xu09+FjQk}yKslZHmX%HvCSQyu zL*h_~Qv!5FqR?Tx4iAqCM`^)wU3>Nx&!h62>qwh_?MO~4dxw;xZ&}}Zph#<#nB?&P z9gbN{v@+{e#?it0YQ|=~Pxs>#D=`_4^P>PRmKf{4a=b8s8lT9)Ggka~I09>rpf&n{ zqeRq5@7`EVIHlGp?j6kzq`>4$eN3n$+{w?iORXJ@0}9Cte`u;~0cSMG4m=Q|s1y$O z;>Nabb(c3YC}vCs<g&4;{LK%VHKwJtCypFcZ{D=j#nxF1qtru?DpuChJ!)U*{Bk8% z+iIpC@5_$@mmL6)6q3emrSGgQd7W{RxfHOGm@CAy3MH$M$-3h{>F)V!k!{q8)x!yU zNjazpDyXj~c&;iT;Twx7$_V=F89Alo*!YsNoTBGP(o&Eljv&Ko&XpZ-mds(AG`nFI zN8v9C451qiaTm549)whN)6S(}VK<Llxun{PQ5?Vz7q|w$y@5kJCf76blM4<6lpkB= zJ%O8~yr#PN0%^Gy-8f;L6l9#0*`TqvT?17Sdio~A4+5pU6~)=v>XCOC$)-+tzTxQ| z`tL&H5Gc4{A$3!NNK{Dv(aq^xW=awBG6z%sc^r)7;MGIgaPF822C+X81cH4<J+Axe zHH@#m#*3Rlpd(9gP)mB?wh=ln$Y`)(Rys!u8(8(CZ3SNuX&U+HhUN;T8-8O^HSadv zzDg^=lEnGmKED|-la^(YVHeP4$51|@Zlg*ezgoKxd9n3UT?D^gZH0~^5cU(f6#gp^ z_P^0ceVgt`2cq?vwkB_jc2$h2qo0X@!1GuOU$c;7$EE8dCW{)yiZdIm5AZ#C$q)@T zooxhu4UVO`P8GQenI~Liq{=7VswoEmts;)lErkP6o$mXxM+vSL|M{bIsjXzJp@-m6 z$qN`lAS@=iow%VwIgo>Vy>W~e^Hqhc=3Yq%WpZ0U7yw15GA-8yDI%#9U23(XE&Z1+ zS~O!GNeMS;<0OCs%`@@_<iI09DjhkSyRA~<ORaJ|KgEN-P=$>dPk$yEqy_5nx3j{U z$~}qK0KbXwuPO%-WV&R~uUk%mS4_Sb6}#Tf9aH|@7p~kYGvQxGCI>y!5=-D-Ab9@& z4^9;bOkuMKo|L@N4}XCwQ2FyZN@Z9D`=l3EXqR(%fecq@<flX{$K4}dHbc^@DLp>5 zTdo@eo4;`LyqiFR?XR0SCtZ}d_j!PYnu(RaE1jF08`PSPU2Th}z`~%18s5hYlt)e% zXt#(KyWaVu(B>rCVX2u9K~~Z=dw{#5(uy*+<Ms5_60BO(A3Zt(RG{+gVab{e4FQH& z+#Hn?B;G}-q%%V=9KmsExh0-H8_W_$QjVAMw5o;ES#rTS^1N|sEoRvaC881^e%<Xc zz#X5Jcg2>eN}=NPiAj?B_4bgNImt~cZ`m%Bonr@Wr7vdNswHd|aEhxx33$~^w5hZd zymSDieS*L<$z~<nwjIwcCUuWr1m|hPPvNy(X#FyWyo5~^X^R$8>!d-Zgv(QsUSh>R z3Jyl|^t_dAx~?UL!S4jV%H)xPexmN43#gqgpoG^K1!Iv8v$ETvTk19H`V#|PWO_G@ zhD69q>>0oDT6_%f;><?XJ!!JA7?NGR@c-Ev&moJ~?Y!TtpE)@O>8h(TFAoGxwlb&u z^wW|=E*`32pr{T~2T=2Uh9^-FFjE;7#&TNcTu?+Hx;l}u$YCYV>2vv9vSC77HE@j6 z^q1M1IMt--z$?^H-;6zO1h#w<3{cmzioNO!dgl{HEk=wMFOjc*1b1Ixqt0=6yIn7S zjYNtIMr(zbpL{cl!yj@794dy2I+$7jo_g|u859Z*>=j5=0sHR>TGv?;SOXk#KQsOb z7FBpJmG1pAii2Ggu}95Gg12+p)(0~kveF-|qPYSdj4Jn8g|r2A-VfhXRu|*P{<^>? zVG^n<{v6T<Wo97GR>Qq<bv#5l8ITgxcO-Tgm$Vz6UuXE>1SN{m)*xR2i4+^q!U)Q> zFhKPI(xeI${Eo3l?PBHKdI(&!>OCUN-D%k3Zl*K%&7@%C7Xja@YD*(3D$Q9XtnB;} zuoZ}+{EHRmES;7J7Dq9ViFY8^CIoWhsjM~t$ssTvJto%N^ZxoRYakt@w$N||(oZgc z)9_>j+=z2S;}W9{Jyk}p%QL=6*EF~r*>qg1)*+!75Jh*CEz4x6R39n}!!w5!bVNxJ zTw8P6wXJZV#5$Ln6$^`!0d72hop{b2IWyZMRud1uKunMRdb6z#WK35g*+h$T1wK=6 z8(7;orz;cTR>h+$sCOvKU6o|wB$(!W;yw<o&d~8MA#+Xz(0MtF-J%Egex1KmL_c0X zsIddE-Y>p23jNlnNDN@yipo%G$_48vZ?xY)s@4@2-tN|_5HO;^H$K^vH*_-I8)HV9 z@#l;+#zjF-yNnYnG3;-Uj!&&V*rqc^9stY^1<Z{XMKtA47#w&lh&~*jwT@uNLrFF( zsI7X_GfugWD%nM6IZlJ~wC>ukL)ixbH+uma^13(#;=L$7?g<~vo$Uq>-#HHml0TYc z*R2`ruvTVGM%UzMJ!$IbDBRl(hgLtRW*eariIG;bCNOKjxob4l=VlSdt`({7&y7uX zut_;HU{sLLOPJlKeNnawci+C%hf?5t6l*>&$azuaCXrJ--P6i-xc}c@D<0lAMzti5 zVTe<;{Ds>;Z?Gi*;K=tdveO1MaIQq=u#-iB42QeU87${|D*tk)Y!s*fto!8^8GLNS z`21n=n8p$UT$wpkKZwMAo1erA9ZV9Z_rhB`;`k<qU07aq(88F3w>olRvW&1|6fZyE z<(df#gh^0z<L&8m%yNBHy)7g}ZbWC^)K;-;P4UW(v_g;tnmF1iDGo#dNDS7Vv4c|q zM%C!|B_$G<Lu+HcwDpD?p2hdSZ4$#cEM>-!12oU_a35#NEOY*Y4sIq+H8M{UezX*z zpWKgm`OXrPD)!8D=~5p6YFNG|S>=J`bX}@vHKO%XV*(}DbSU>UZ52To6Nla-kMXFL z%>NA(yWI-WUMEmO2_!u^9be4{w%uY1@-f$iDgb*=6rMv<9;rkEL}63EdQ-Le2h~j; zAdj`Rp$La1w{mxXhdCPKG1(GKSf*_x#$DT$a}@5j2g(Epr|Z#CN%*KiXJu(bYY8fn zV?LxE#*a2ofkl~3xperspi`ZS@z!~c7c)y}c-f8%kiH>9_lacHPmB`^(xZ`8=dl9% z_@F3ycGLD3b?guf0(mbpcb)(|>9js<7yXO{?1e{jKU#cNHzkSYm^zPQtqiqPvN!_o zNT|efumn;R;%Z`EK<)_3jJTCWu*cgF^E}CSr|Z42bA8JVTg6zZI}8D(eiJ9W=*R{h zkTSvPNu}bcfBaD*OA_kutiMbchs+kHumq=OMGfqNMwRq>o~9R*_D-|s3FXdWDZ}^n zI!KQ#Je*#EIBwRj$EYU{8MY9h&m}3xWGl&#Yg?ZnT(j5qO7X}PEmEbg&4G>}>d+}t zfro@}1mTEKlyO)FJ@HA#L|;KNIEEPpEhTxtA*@3a=k#$`Mc>xDT~QS8k9#5eFc*2T zGk8=83)YtlX1KFH0(xA3caieREWDwEV?i}o*k%@jnGJ(2(3kb0l(oXuH>0&XV-VRH z-vWFJAL60%oSACc)U#a5bvsemo>L(QhJqfIyB&xx0&+5JV`m5gH=L6QHDvhy-B*wc zdi_==z2=ZiNFc!*q$7{w0s3%2L_&iH?yv}{2&8tUwYT{}#}Ja-HU;VnH=f)IuY8!r zC{Q|5<&~hT-KNUnWVXA|jc!++1skH*y9Zw-`YbM5<u2hZH6>E(8%7)zzVfT`QSxGi z-KI<?OggI(`(iGEl;$G&%Cnbw$g@X-e-{(O>Grv9pd$}Y|5FSH(UYb#R>u(enJXEU zt<L0Yw4URWX1@&~YvdG6l&WBsdmEDlbDe^9oZhc;TIy{CBiMwy_npmZXHQ*XjV#^& zSgHs7u!OjxTn^A6X^%if&fFfYQgcsh$~UQukzFzl*Ah@Vi=u&aZVeO(?;kXR+JMZz zS;s(+DHFC%NFSy)?y-bsSqg<cJh5;UnN%4UHqWDCa-?Z<IpH%&s*6qpmirZpy7Vol zI{Se=j!CY%Opvj)8dZc~>XtGQ>(-eVJ;&)^&D;e{5xQP2XZ+5!L`QCm31n$#_3V=Z zf#EAA3q6SJgP+~?8sy&p6h}-CD?yna`c4Rxd;NoEa*u^S{~{NARC@flHnA2oNb>v) zJ)(*wZQO*T5RPppn!i^G-Az5JlX1@2V9DY;XGg&(!v}BA2hG4_i4V<$u>3Zns3iK( zx7SW^`My5Fa{AZ_3JE=_wg>J}4Z@XmcZiG|UHbruNl#vl!5f<LpNu-y3gQ1-7hpMW zQZL+pTwZJpNx!cN_+{kTQrfNVJMGB7e|^}S2v|m#<9%{$>d^0Eik*P?;%Y#fyJLT3 z^jMb>vLWu2M*yysTdc)eWNuO>G*<DBJFu&URgK5**(hAH$fQ0Z%t?2oBC*~<0WWC< zJvrg;wT#(sv0`)yq(bANv2QS9njXTNFZ&&d{65OJvsv-l(rd+zM5FKT-N=y!T}vt# z;yDNvBECUP#;=Di91QNEo^|!*<G&bq>DI0DiS$N+olq6Y=~ev0TEF=VDyKP#j!%0# z=Lh^@2@8NVl~KQKKszUDbl&<d#>+gge);a>u~(n)Tz2xt9;-VP#<j}oRFuVGk9#XE zESb(Gwf$@ZTE`E|JYyeC&&S*+!5~}%;dI<e{6<2E$v_YnZT^fIF68`jwdtco*PV&w zs?sA8(|)?$Ac>UE`Gha?V!A=i(bPoD0Y@5UAaau#Liva(Z)4C!(|%Bt8?w|T5R)Dy zE-fz*uOypz0Q2>*NyiHBnx`M54SBk`O41n?ts?PusNj0UOU@Ql`6%>hKlmHF@!u`` zMZb4j2)>OO>0!qW<wqM1+I&>I#~14jzW#nv;ez!g-VPwWQK<mg=|A+KDj_7fuJEwL zh|RgZ4IDg7bV=o~;p|2lJ(M>E+dYvPy(#CDvEhOz7BJqz26S?zq#ZNM30^WZSSlGa zNfWirdfTYs204EbpN7E(STXa<M;+(<oe^G)8ZMlF`)acn^N+>&2a+2mk1s)_l&)-h zAwhA`_sjp788r|C+_YNoWAT})6|uARks?iECQ+cCq45qG+*&+F+kYPH&wKVjm1my1 zENpQ5X4Ci;uQ3Lf7uSM^fy`77WE6+{3kKg@w`HW5C^YHI5(-9fh4?B{3SXslhGbNV z<!W&ZQBNjby?qB_DaYKX?T60+<y8?#{Y==$NmsN8ya*L&nRsCJE654h3{{IfTHVPK z5YO@1&zTx{BJhrUYY?j^ZLbu=lrlrrNX^zH8RKgoQ0S@pt3AZtLmKB@jA%;^ah(8E zNC{BSbvw?+V2+*bWo~vzpyWQK&P1R<FyW-E(Mh%!s2A)xEea{dB0AL%zbpZNAaK=h zEz`0z5dX5yo9(ioCy39YV<D_yJ37>jIKe{SEp$_7g0>Eci#cu#P?mnhKQI$H${95x zGhXms_4L7-sGB)Q3k7@@A_yY4Nz^OIEVp}|O9kf=uuZBEr$iM_SMcjli(S>MuC_)^ zN_~<nBaCx(7`N3T$=#6uk!IVB>VvrE_Wlo|OF4c4^e-75*SrsxetgJEc!)~KJi^n% z*qOJrNCFRir~xn*g_w40?20ZG836c5s~fHargs>7|F;LascA$tLsU4qCUCQ%c)EcX zeT);`WDX%GBePoe#~5m8DUQK@!w#_zbJ)zqu1Uz*ghH4?Q%vPICw7ni>yrL#aG-c- zC%OQzzVFxl$x$TAa4H_z)rpxEEASH6h|!ppbD?Ffm8!S+A~^zvL$4gg>dfWG?)b7; z0y^~B4nvJ-_%}<xTOv~qyHlq51$HEQ0oh5vWmR3K^k{Tj!c=dlZbY;fYgN?v6Ik}3 zMsJevu!m1oaJ?24T909>1vwcxRS<SHL6ZGp`UeaimlTlO0Ci^CJ&&{r*>;1Bu4a9n zi4bA7YX|Tc>1OUG(7h4ZP3S=`+u6#y-gah##VKVuyGAGdi=c31=Ih?otL|_52a!(w z&pRgtbdg|cpd#Esv5u^FQ2IphaDv`3@mPMZtzJmYDaM4e>WpsDj<P<+f}BwmYH|4k z<C6PaYKfbhrqgNm{illun2eA7D_bV+G5vvXQAZV*WPw#ZcvCDhX75bE29Y)Z-IW(e z;kIkzq@D0rn4255(Rrb2Qj|aBiz_!%>lpH3MvWeU^jQgT2&@%xgY1bDO=PRDcQ1nM z2s=OQMd|qYQQb{@m;@@0`vuWf-5I{5&>((^kYFtG)-uBzl9pKw+FFeJR#$6)q50?u z1;YR`a7SKa2>6ZNU%G57Lc9MGu{H0WE!I{?3u%VTxidf)1*CEPJCxUuBtI3JReq_X zJY91IKjENU5@C>2wv}~AL4zaDfKhBUIo!DM$}SKF53bfK4K($%>KSNdynCy-tMnR; z2EgcAV(AQslG4Nol<j{$Zvm(H&nG#FM&>60(>euoyl}wH%MsY}$~!rz^`3U-$nSLe zJhc#YU%$ULdP|hBo=56FAZ7Oob6@cQ<>j-gcXhx0)Cq1<1F2La^fYeBihvru8Q_$S zn#VsF{@=a+b9M7J0V$Qpmm(7c({peoZoj;=@n=4IA+50$kdD=HrvddZu38Pxhnnqm z1Ls?!$@LBayAtN;hB%%Sv>>^F{vLP1-fwM`Yk7417og!MGE~G};eCQ%7Er8Gsv5Qm zSe~lQQ?!x?5UtMzf4ZwI_kkhq$Jk_xn0P8^XRQaOeZ1RO8MQYFYM!eKf>N)%v)3H! z)kXXE4*<OgTIkC)LRizK(wlIv`kv<Icw>Ac|FTi+cM+boB`O7+*J^03qs%!`Yl3aP zGv%WaO+V&J-sm+AfsE-4#buq!3+}ux3uKxxGRI@mB+VWNha(lI0=GN#MsU_*Vnlxe zbL|?dvT0d*8ENNQwAZ9CtUB(ukIB|67GSH)-wT2VTw&!LhQRnl4j@PJIBz!tXb&Em z@3A3UVgB06Pb0#Ijx!8MGn%Kxmut*`t&cN2rUudiBe?>U;MsSx^gUJxR&friyny5k z$E_xq@(cN*(I0yu#oJtyW?`Z4@-Mk8Mi;F9*G`d|HazMgcN8TTu}N?<TdspdPV@2@ z0+4CQ$#U@X@|IBb4um2dG$L7@hCPdX%MjM><6EOTZJJ3DSx$h4iFaWLTZ<5t{-9ud zZEEeT%*^>^qxmzVY=(ILpPCWT4X}Ik9O7vKnvwhf3|Y{C&X~!SGSuQq6IRnSE_DwN z%5?MHqAgAvx)yOw@CqG!!B~_h$CzwqA0wR*klL1|RO@KBF_fN3QJ^}U)^9ga?u;D9 zim^w;ixaZusp%vY2}DwN(pBCy6~kr~x6B*MfYUOh+tJ&vDoncy$q;%-idcZ^$?&df z+D40exxj6`Y;pNF>d%6opz*;0ZVH0XJf2xFct{T++4z(=08vrEJ;SQtDp`$y<Lk7a zBoE24pM4hJae;xohN)!*eYOj6*Xw#nAB8+|i0++b(6#ZPh{lf;iyTVfG<#2%d1wIG ztS)j+c0FORZ5>(}k(Ozlg}+{%1HKl)_Z7$bkLek@j@uwjV@aO_mJ2V)0ByAeP9~t= zi=8TNBmGVjKKJNMRUBpqL?t?qAtKROM3WGI`aoEY`N;hMir!lxikq#5dqH?AR(Q*+ zWJY}%hBf0YiYCR?dOM>%6seq_nWJ+fRAcNbZ_lWrHF|2ywA){|48hVE$6iExiCHiK zkVl}kZ9HG_XU1G;Pp)L=4OhHb)uH}sk*sjBEA>|c`6uxy)K?q$Rz$4XS1$Yk@a_$z ze#c!&0y5PTo0{$CP8#d%+9LG}Z(6|6hS)rvd4JYpmk|keMG+<G5{VfO506SH>)8q8 zmio+*>o*AedWmUve0_}E$XNhWWk8vxsNwJG0@in8Lc6ZOj8j_%#Ft=y+s7C-Xz$LB zC>BZUNmqu6ar1Aqi;I$JJ8@AEJd93YyU)EXORNL0?6xTg1AT|7L+l_Tc-9G_Aq@8h zhzkfEQ^N9-?1-*DbG6T@rZDE=QFBE9+vL>R<@@*uNk!ViHm<}S<%opCprGl)lcK+# z>++x>eY|bZ@e1HX95_g83|m+;6ZuyIi9oIddIYe{gSTy4$_JXr(T^-P96m^4i?A^? zlM*K3JT-1n?)h)xQI{=nc@TU>gZSSTNqzYR`;SNP>!@9U<`sVMGsXB1Ec~rEz+JXh zY{MP3<Mtv8VgMuL-wo7cD>gS0K-!NMfCDM^Ggs7Ra<k;CsXp&SD?whChpRMCQpgN2 z(}`HDYrA8AQ0BKDlzN5@mW+t_Ny!#XnT~+hH(Es?=Ve@CaRaU?wI)Ql8|U?)M$OA5 ztr@UX&CRE0<r_m8SaWjrESTRp{4M%xw4oAEe_&_P&TxQGpL%smvDC@JH$%~p3FP?0 zbNt9gf64j`rLCzdHP}@OY%Ba}`yTm)A~gFzRe$oCEOax*5JhK9wcB>r$1ffbJAf#t zKHZWM@k~Yf7%PX_C7D_mst5qH``&ThdVUh^0ovX6f3z~Fe^la-MsWhL&0<%(xw?Y@ zh3!-Hx_5%i1dzZph_#ow)cd_Xzmcomu_u4y$;|=|ra6$U*a)U)UyNrD5m}}Kwt@vF zX+<Qb#Ib$7vQ<WSK1`paQE7RW2HNECH`zJE*c)J0#Y*Ktq_b2{weW}r0U^kexCzvH zX(@6dT?;hiPBc7-se^#2N)jy;+Ce}3r6{=XgS29qFy!_1`lLt|mh>v}wmm)_V?6-p zo6u+5@U(&3tk9VRsF%(LnAhw<QU)oy&p02T=w0RIK_aIN0+=gfJvUBZ{1=4NJ<|~J zM33#Z|BgQf{3wn4tte74W)p&u(m}fMl;_m+6m_!&l1`7Q*6wHzX`v8PJ`{PqDOB)f zE|R&oQJFhX+P5n**Bjp?=4lPCtmqRef>sy|qCTvmRzh$X-f;#Bskh$pJ+)GlR<()j zcKbdWxI8VgYFeU{#HwPyXcJUlQ?H5y_Meu5>u$Ct+T|F&yq<S3rk|{T2-0Pt=$d}4 zZ#1l(X+-Fm`!Q-rh6&DNxrICk;ysl^wV}mBx8{er62QAnGM6*R?e~JGQH*TwV31O+ zFNrlsrJ~Rt)L5M$Sx_w(dHD__$-4$DSrGU$(>OBOfWCCiVF<F!hu%&Jjr$1%g9oj| zmkyhO(`DnC@5OKxbS>e^R((86^9s3WBGBU?(VU}Y@Cu=|P$X_ag_H#OM|vRk_g4}? z80Wc&I%^jdTi}HhohQmzUZ7@&S%wk5b+;Q(K{cNVL@TuPc+aPG{GJcWdvQ|fqsy03 zhC<^Beg`L-qf!8V1tNL_Wf7=g!@>^=s<d%2H^wjY5GTQnXM?ua1B4bKTY(k-wgtl% z6_+)qHLJOL!1kxe3f0ip;js0TH=ED-bSPUZpn{gg{c`S-l~;@ZQd?`(0Lt$bAQXTI z&Ba`dd_pMVP|ac68+O>9B3x~sw1A%ckf^g?+$lHJ``gHfQ_9ShfgZLI-@VFPo*hW$ zpJMnAxcAgt&4NfoUudi%Ho{`7jdD9XJNg@-N?1Z+)%jqZ{TadxYe%1&9}NhTfwZ1h z219DII?(r!V|*OBFM5~!d2h{=QYE3z&C&u*xV%y#lD?7?Eg5%21W|0Q-y8kwnPB%C z&!Q!gneB@ho2WoD-GIEi!ukPJ#cRWPB73;wyP}2*TwCaKsy27;&r@Lh7iLlWe8q&w zQ@VsDAzf|JQrP3lC~fOk|CDb1t9c^&-FDdy2<*G2|6P+)BMgEC+Y~B}U?y0i$V5r% z%`dV<UMms&+?;t*ZTTybn;*zgDNzs=h*uOSLRgu>Sb#Ob1@mccXIxG^V+5j&ds zxV-=VWJN_tx?MF3Q6hgK`tcPCeJN!0y>?3Y>0MS`WsO=!k2q6a9*d(Dsk$=%*={vc z#**j|#c!Ewu*I&|jX1Fj3Uy0L+p8Mt)9AJ6vv9UO%dCFM5#IHo<B<z1(w&2xE*a$d zBZdQ6`~Mevt6Z-H;``Q-^pS+av7j-wznx+QBHBtW-Sthe5QH%Gmey>yae71V60hl@ zAR4T0!mq#x;U)(<jJNP6z0-208BVoVO0FJP`U%|obk~&8`YMp&c4x^PO|e;EC!-Fi zo;QdJ=tA16%4HIes;E63tNBAek*Ec?pJ4hx<KIfr#(IHezZY%qPsmM`+-u5El}Pal zg#y~2D+@&`p&@k0&0-UKR-NX8seD2A81bLnLQmlE{-(vAK|FE8LPIr9P_W<`1{NUV zDB^HzV$n)H0uy#R*QATM1bKUo>qpOOs#@s}3j}M$`q;*7|Ab!D*a<uoE`Q29(X}<k zK>`uncnAjMDs1Q*rT<4(Py#^^oA%P4B%w!CbrFR&!~-kd?^`$+ejC1@Wf-`|IL9Wd zt1(lMI0J^6bOHbF|L$*x?JAo~3}Kvi>Y=4*<+kt;G=;hexbu*+PvK4dUG}kXnFoPT z3O74vc&@&k249>55zYo(ecp;%nX7BapE!~HZm}8*3E207ub6Q&X^OmsNKvpaj-ID_ zn`hApn6~f4sCG<RJT}MfseTu9E$Thl0H**Dl2x>vr_k88W!sFB?)G<SGh`V?4u11? zs(msS)_9-s9TCjVH%vkK{)#psA{^ik&daO|7@%aGc{qk?1PEP;L3?>^wUv5x8dq9C zP_RnhUBk+wUA(Hg6HxY^w=SgtVJF$Fo>!$MRhHpxQGUy{?izWnVQqcf30=3T5aejO zpORp&(11l2A>P7{(Id?enN0*2;4k7!WP^DH)SxK53F(i^<6o@2`IwA<$k!Y6!v!i7 z+OyyMz~EUrfxQC;jb=JXs;dniyF3Y0amiEyS<w#yam0~P$*8wfH0jvo8yVKA$XAc} zF5+u#u*zNrt1Uk}mYDMIp1I_{tK$;b=D1MLK-q=acN_WKEBg3aWUl<hCjVz*o|sR# z5EUH+E=*Q{$bY`(#y3T`2nfmG+Md=lDE<Q=%gVX-gT}S&0xPE@&)tQ6*g3)mRqPQA z5a*6?m-W<j=P<>R18qA+AL-hChp?L4Gj_grD?2mwJfeRrcW+X}+CSVuxSr?^?C#HE zm;11Brc<g;GU3sbBlG7H7F}H;G(I&R0_eQ)(4ua?8ksIIwqMlVEwncnwaLDmkz8Jn znO6)1=oU9U!x#>2si(bkHL|r*WKs|P|CPNIGwo~ug0XUbT@)Jv$_Q@wq}il{whF*- zfv0B~Vs+W~t!bV5TX5yH(bRYnpw{a>#<iGL3|zmH(EL3OK=^HrHSP@tqrXMf<e;{- z_$=e^h$RJg2<_TM^<LdBedR8dUo1M)*DtB-SIZ<3^};Q$9x)S|<5-%@kO=UFRwnrV zD<f93c|-4SAgRoc&1N#PEyU3l)N{2mL%&=CCP7i@7fo$SK(iqT+vhjQu%@RVX`VEm zL`GQhF3S_`_!8?A@<)VzIbFKZwdz0x<tXz2^@Vq5-Lxn5_IzBF2wK*E-P`Me_^;<k zBN3SK0TbWOhS!g1UlDB-#~<7SZl|t=7hMudS!5b!nnE|nz{0YXGouT&;`}rMJB8vc z7j+ljogr@BvKhuf>@DCGkfA5?kqcQa%HJguwTjTxBa}xD^v)bLp6OmwMZFBUj&Pjy z{i8>nYnMr)%0n~{(Nrzrj{Mo}DsYZ1c=r9fR(MSS@dglC3a2q2Y@9N#)Qhi-Y71-U zMWK~^%<lFN#bIk}t-IN#54~Zt9|12P<Wh6=yhtG9KM}{s6}N8i_=8Hl{_fy&0>H_5 z4I2^*cbXT4)JO7O5gU4{;5BYc0*Mq!8QT={!z&BZlKX|!8ViceTYp_&8E7YE&1jAZ za>QBwuMdjuJ&e1#&9&;e)K+TrRd=^GUttbh73Nc}pTs+RKh5A=$n!b68*9V|AEnR$ zt#0N;h3bc#Dg55&MZQ;Gem7Z)L)HA^O+A1+zubuC$u#Y^TbelPKM_6{=+&lH3QH{T zclDNi#WsDzGH+?hg+opYC&oqT$W;d@6b{Wnr^ze+MZ^=o8qBQ-?=|6P9a*w6dEJhA zO>cf&>w07>h}Ws6vJFJQU6RsX>zfFJR-}-2FRQ6?5cDA#9n;tT`O;|urozg!ls2@$ zl4Q2_zVt6NA?{jY`|y1xJl{Cu#621Z(d%X4n(@XDO$O<ci!pzpCoIzXzI!n<a}{Ul zb7tg=LwvtDf~L;b#x|6(K9NWm+dN%Q9sytkJ#6i#g=4JF%Y9(^4HmgpBpze{-e_j; z<lkwe_3+_z_IC;;EktmGyg_?O3WTi+Af*Ah7_ZO_YM;CkT7ybSlKvj-X~!FN!T)P$ z2(5boX8mUOa=kSc(`<W#$tE=xFKETHVA}IZJ$jlTz9t?xu}|#bsYGWM7Rh#<sGW4f zuH0VspL>44=G}*AW^fMx3rpg#$Q`;(Ov6Br2W_!udoH)3F0c40a|qK3dJF*W%-0_1 zCZSg%i>u5pWq>>wVTQChQClKA#yq9|XB~+;Z8v`71*!nPbo>yoN<<I8v#HRzL7ov+ z8<M-pTZ5AqT;lWAqs_Jp$3YW3mol&_iE0O)p3>D$d{*M-B_sH^R;{AdHqGW;rEWsn z5y`d*DJQ>Ei3uYu4JR?hz|3P(KXnKa&n`PITNe5NRNB|BA1@#IQP?ZoW5|ib3A&IL zId{)e+VCqJQ6?Kqg8m$=m-u&b>Ky<V6&E?i3vx7hLx9oN8Jot*|M7(4Fp6>s@3N14 zjo&s8EXslbTla26sXz*!D7vOBYLL;FF&c%#-&JcB$puVf;b;Bs2S<JfQCpkz4jD@M zQUz^`%tHauU3WpfLDYAsj<_ex_|Gt&(I?dLdB-2mBQ!K-M%9f8?vcXro2TKgP(RpS z0>EEkapTmyh<B2!^rN?I<#~T`<2MROhuG{xLp%eC!9e5ym6Vzp)qv<pAu^f-9Mk(X zvyO>#GlG3g&O9C<{ZM^9fjKIyy(U#^4}Zwaq#b7kJjIR6q2+eQ@gDR_N4R!}GtmK) z9JZJ=+5Ue&8MT!C=ZrwIz|SEiP>#qi^w5S0)7|Z?!#kD%G5g><^O%|=*r;|b|6?IC z^)&QWp=sB~@q}{`m&prE=9qJkAzr5qQy{?db;p1B1$?z0rbutOJ?RXn4qKw0wo8p6 zaU$iC^6;k&vaAKZ+hU{E4&hh{JX^$);O@4N>a+roQlkT;%s)i6aY0YXZQZSf|9C)} zxCJ4zMaOFwI^Is86jZzp)yk2e*wS{>BF-xM7;|noytJ=B>VAJh%E1I#;@}{Ir@I6> zD16gDUL_Bh4t(wni)zURiM_o(1qLZT6Bsv3|F_va%U*Bw;pmYr^|p^eU3N}$t+B~X zY(vk)Fn-AufIFR`7(4ixClvn~D6A60kJIPadJgzl*m6r>2_8XC7qMGKWuTU3Iv1#T zWU(F%{`IFWP9G_Xs|94r>=?rF@X7DlCr^xuyf}<eiaDQIK7@LDx|B1A(#{+dzWt>O z-^JhKKtivj+y=vKey)@FiPcdQ?@^<3{#Wbxd2>EJ)3X8oxdO)DF=JQzwr@-n$^?*z zVm)Tv`Zox%>x|u)F_2p{<=c;m+u^pxS<GcY>?XpipUac4Wi{_Jd9LsgRyE*`D^W88 z{|M>yJNGbtwTM|QOh4vXAw@(Z<!4L3fp;~oO^$jK<baSI#<O1zFxbwio|8Ud0}fO~ z%75);#)OC!YAao(ofldi(XcURoaW-VEF3x3Z$y6(0f6TW0U#jNAP8}f6q^?%+f|;H z$8UZb3B^!{n^$pMWH$G`CDus0s?SQ`b3J8RqRajSq>{^znJ=s^LcbBc<n*c+7G%-@ z#$VNgSw`Np>+^h!cJU*Doi<n*)xr+U4$Acl*u=>^4;L_^a+5-5ImuJ6kdkEs+pQb} zVP!iyOsQ08s^Hk$Zy!9krN|&t827^uQQN4p?HZx7(QzQ-zQ)~G?itMI#@W#a$wnG1 zTbs;;jL~GL_$kRdv9KaQi99|EcY0wQgh*s7nfL&E_=1ToUJ5z;xOC#_9ADCX;%ztc z5dNM!NefTj@WmC9bF=OiLh)eqqmz}v{__B^VTPzX9^qk7O)G#GsMNnOa`Ky!cXJ?2 z9Q92s&t#5#`5a6J8w8qromkVzp#{o)0f+}kB^JTC1^L?_4M}+Tt};eE`_5ZSrBL&e zE%m@J&hLL1n!gQ=oqJ`sxq)6Fq1-oy26V%o@Xgz(m0g|Kr)?HvA%k(0>X;pu#<Pw? z5Selp@9BD)yW_V32Oa#vI|Y;SV^Gj@Ek2MMhqCuTJdxZuNy!48Afri}zaib(7#ZIK zBUSv7ri-H!NFcquLHIU0r3!6Nrwo_5euiW$S&WE@#a^xVl?05p>yIb6{UMhHVRO-f zN9wmbcfX=@Uz8~zXmB2KZILMfzjdmpM?n1)W3)&6fix|YFXC<^|6x5ACp6K_cLY80 zgO9rB1}={U|J@vNkzDw&HUYqEW1%s|wyK#{$Oe50ZlEEWvEj%A%W>H#B8Mt<_av_# zzshsa)vod-Lx<}ot<dqX$%Wzl7m3lqJMN>Q$JKd|FrJPE?S0y1uM{}-ZWQf2P){eN z1_GEOx9>_?>^^k1E3}siwJYx4%oLGr9p|B%HQE3fg)NIH=Sb6p6%&8@)bgfG#PC!< zUGbi?s>WqSXXp)ZkiDl-5P}Zh8|kE)*6nr%G7yf1w{Lv!C(h=q-w$TL8Dnfo6=<VA z+Esm|Ty8L<e?vgrBT$(twgG?y#M?s{2p4z5NkrR6q~Q?g2qV-X<rTJoVnbiW?kG9H zR$8UNK((GhfcXCE8U-3|x<ud#J=L{T*5|*O|A1=Q<CO4`d^wv*dhN>8fmQ48__@uU zboypr$Yk8Fo%MG|^Pyc2UX`0En~#P)4MJ6|08K!$zl>QB8^>s}k4__d(r*KKcvb$a zZf-S9PeuCgsqwKrW}mA15lfAr3k*3*=G4&4`9^*t;G5|BgijNmj{5Lw0NJx04(pJv zsQg8gKqZ`S57MeX0EZ|L^wS+StCK}!2ZC)tH$SvB$^4XmCzVy*Rgp{55zaJm7;39% zhE#wTv-M);83FwC%NgP`yF0Fi)mZYA5bi<`npu2H{=BU75STSKX+I?x&Ar#m+DTiv zpQrWw2GlUS&|$tpfl$fx*CyveX<T1?Z}9ns=n_MO_7PfLlz-Os1ETr!Gu5EJV2xBO z4C<6k7%|4^o*v5wZw5*n2obW6!>SJ~wF?Z1pwl}iv6H6&g!4nzD49VBZvqb!4Q#E5 zD(n;QwH=qhoB0^}5%iY{pN!CO(z7B|a-{nd1!MdiyS|?C6aHvE4ByFm2K4%BvikwT z2tFHlW4h>JEAp~qW^6oLFCZg3@dH9xfwrMHGB+>78l;I(Yg{+)3q5V;enR;j64-Hm z)z=vd$)+Cu0>;1JE1P)ZjgK2Vf9)<{Shp*sA{C6yZe!a$4L=g-y;y#CPFyyKH!O@G zH-B;E6zaYj|8Ke5d(U_+x3EpUY;mfqk%R~bSrBpk6|8^4S+~>gF9B;+F|P?RWx}mZ zaOp;m3!{2v-<o(`EnJnAgS&`y>jvVM*bplU5BfnPd?JezpNW?(R;Y<6?N^`Zs2omc z?a50`@uTBrK1UX0cD+2pyVZv62m#vf2XZW3WbR}p{cB;EMAcT7q?zjl?6p|t%~x)v z7Ds0c7R-ZH)ON9HTE5D(rnw`>3=G-8LxKk?vLteBuXMX2tWz6vTru;GW_baRou+aQ z4icG}yhHJkt=@O%%^cwB3am5_J3qRf)laR-DaZ9S#l4f>uwSe;@5;4khs;znJW-Hq zw2*~!JY(`m`JWi^3dnef{<Xyd#zjy|NTMMJ;IoDYc1YTGuzEOSsn}xi;!?ZjbKH(g zyQ2_g?L6|=8FA+vCwuEJbXgP-QkjCTbJqaB>>w!inZ-sUomnLYZ%@@+ge|J@i-wiq zX695?6egGx&J>u|Ig#$#_Kn{(H8qTx8PC5?`GcOwwtQ8P{d?oHV)H(e3PB3T!!AI> z1PI>Qj(0vbjFwG!b&KF>c_i5DO!6lQ21o6zycPwL<PQj0z=Y74mExe&dUC>`53#g- z0P*GQm^IJ~Vjsw6_aVvp!@j$+cL%eh4Yb2kSNeL8HRR&Gyter%zxg`r2;cD4Pg}}p zUscE}2p1U=4=X1omow&>ng79lZzZiDcV+f^3Y@RF&OS-OX&*`v6JamO25dQ=hs3>I z4jgFvdtgqpi;<b9=Uto<nreXn+$Z?Mf9*t_U$IBMa#Xia8G$>fAL*&%ke0TbAmdxR zlQK_CtW#Ofa1cIV!ZY!rdF|wIG7c%wX9k3I4^@S?7mobCh7mY#5MXB7%3^k_*QV&z z%mJ(#A<^u@yyH2)g`%3H#MGP(u?=}`(ymiZ36i22v>QYuntdneN1#00s7T0f^2aGe z+j8r-Wf`S}BI0cCjm!5a13=1o{159c0hBH=Drj@<`CFdHzu;SG9cb`U&lO?uRk5r{ zN1TSio1OW0$4;a`+#Uh*o-4K^0@`9ItwK*OQciy$GYLzPr5;ej+9e*!4OtX;-z00y zi;iGhpoCAj_eO}YlYagE1;S3dKWwsW<sC%zx5zxihgvB+GW-hLA(w3@@nj$T-g~7- zLf$kxwoy~5ZQ?b06tQb@;8$C5uGBf{$T1!LQu9tKlT1Oj{M8@}8I^qSuNzAd{~P1? zi2BO>TklL=4X`j*e?+X<L)O_w!DCDM3!!{Jr^YQOC7;>0Yi(CQx_$S~*r>u0NLS8Y zQFm=)3!-Iv6_Scvt9c^I@JqyZBM-D4FMt51w7n4)iU*WmgZ}h3rehEPf%62u!c1y= z0BwH(PhH-|i@%gSRk$n=G8wx?JAJ>4BGlz`^2uXX(RW(!Q4j$m3V$H!H5F+~2{vJb zSCer}b8O_PE$XstRrKf0)2)6#MqMOh%jEEnjARIiJ2sSH3%q;&+;DCd1KP|DHro?@ zuciLjSkz<Je`1w{K{z{p!Pqq0>kU|u;=gE|yFw48kdGE~?TkJV6?;#$?O|U#21?<a zr|TWb!OBI!BRgeC8pAiutmIB<IT2cc!Mz2)(_H(qH^|uO2EoldJm;b{rin0R(Q5h3 z7zN6&%^XCLMsJM-o{4;fEm9!yx3BHn%*JtG+XJ)Y1zm@aWAW(ZtwQ?R=dN%GRUt|Q zn*Y@mu@<-?{U$E_Plc8u8c7knA@G-Zn?5L{0No(;dWtj%@Hq{@OzBPGJ5Q3qO5KLB z{M+Pr_=D@JH0l2nJ9;mHxi<}xu|M(e51IS@!Fac*-q1Zkf?^Sjd&8I#t?iLkcaXhy zmL*S=gnN|d)s#kY;5B8buSz$K4KZI7BO;@h^!ix)sk$-F8~f%cT=r$G%BWJ&`|-cc ztw8IjP!CyD*@n{V5GM%F6=g)FLcw<sVnYCRv3&<vlaSni$PurKxe1aw9mnmAre0rp zZ*a;F%#=>7ktB8J7R8Td4r1uIMzzQKKc|l~3DmF$CS27}BV#W|yE-4_fy{~G)MDte zu1KY?)r}qP0k-O1P^hYmknQ=l8{nDewBsy3hHD24HDgJAn(pU9E=1?6{WZ(-e@8iP zO;MhK7R^btzd2|sSu6h^T%QFgL*!!RC;#6TtQANmk~~nT2;%G6^d3++;Fdv-*!Bb5 z0V%0(Zeb9&@8eijg3MA#QOm^jC=Ae;l{vJ7v852bry((R2I%S)6bTEdM>cLu7R}qv z!4>jU=)5V#-KTRU`8btKKub|8^R$~z_Mniz);y7d=Af}r0lXOh7QCG40>}|;?ka;M zEMt;N-@*>Wyy6nVZjEvddJ=APH|Dy3TtiZq%QULu;I^*>;8`{b6xcBMO!+GT+64?` zJjNK(CB$xSSURoK(CTBfJ!5mA%HD*Xbfj39XY}iVmGP?t452lTD^NK9^g62EiI_tf zuJWNtI4sA1$5f=9sJKs^^?vj{&Bu_Hd%mn6lK&mh2cRAvqZTIXpkmwNn0?`}dORQ; zsv!x(jjqS4NAA<hy|lwMLcUe%Y)Q+asZo#-0A6=p#LbLBP-UuxDYlJob$%pSIEbf& zx(srHseUZGQ4{rdpls8>;#g*Dty9UT2Su@znl(D%Qr>~}EqKNmlaHVNF9Z%yscg>~ z63-B<h{<So`$`f8P033QeItDn6xlr43B&wfF))M%)XmDSA`}cKLtwt!U$~?mWvm(5 z&j9P=4bv5Q`f~I(uJw6d3Z^E5hc#!$3|1j4=fdgd4X$B~_GBC9Ucd}SWzB7sTKm*t z>(>e<XPL|8QX4dS1}i*<-OH!xKKa>dkTc(26>MDnS-lohcWx$lSzqmW<vuCx>!8vo zsE+EY|G)aX4wuRL0NiwI&&J|_*OHr3ie=TU-^4m5<rB;i@Y*P(K&@bTLWmEk!{_Pr zpYfY)3?+~HQC0Ud+AE}BQ?2z#xKq-X>I9kXx`);}=Nu4SGiY&*YB9ODu+kLTN}i#4 z1hWm6Upcg*&u+UkDqKV>({AGktN2i^;$rRF<ye0PI}?;uSaPLIn?4eG{RJnX1Tf+m zPyq+x$}Ho>^*~QmgEhz$A{qd!ad6kk`_FA=Eb8J4636TI?;tWfccr2;5g*#0C!6_u z4g{PrnqWDa#>Owst&LYUpc|TqHols7o$AqmM<H)Bro_j}_)Mr7544{ifQU9K*wnMw z;mYDVueVU|zX9&U{sgU1w>#C00pYGU%3q%z)@Q<}BeRy78AEPhem^JPZ4nAs^GuFO zwFrhx=(ueEE#4zIoq@wY+BJ%P6EhzB=&fgxe=o2_6YAkoU~yA+=~$i)HOPqLorv60 zn&><2#hxbE*#ro{qZJx)>FMBwJy(FJFJmFS0;Mh_i>=#>oHCb<qgYC3!E$KaA?A%A z4Q=H{EaUwwFtBc6pL<ZGF<PGGy#V@77;<wU`ih1$3+OwORHQ3$4k_uF{+K>;#964J z)!ipdf#DYzr!?7YsAl=l1BzH*6>a7v;AK8y#s+2wu>1ZsR#oy@FNx4iVUk!tos2vH z2j8?|{&MgsT5gj};plxl7w6+$bBY{EJY^`&8d)mJU1w-C(nmo~s7md1@+D8cMsJ1o zAzQkPLBC{E3LIf!2pcAg7+;-NqkI&K_hGf_FqRhDzjGnzNLloUZWv0NefYv6vDTjD zHb~Ih`&p^m5Mg`E#RprM<iKhd%r3-Q82utu3-^`Co=GmXeKH{gE#Pp$k@&aP%$T4w zFIRL*7@KE98CSofhe=_se|4g}MRa@VU|)XxgV|1y7xJ5@Ydp2I*FhHxV74LAN>Nt7 z5-mK#`=y_gly=I@0~V29M<@$O-alw3>|iiN|Hfw+#Lm79xl)h8dYnO*b3qSr0i-;h zZ>d`tLi0B#f9P!<)QYQAVIY;~5|D^2QT4z&TNf<Y^4{d52P%@pG*IJ*6mFo!{hXqc zKIuA&ZmQ(yIF$Ih2PJ`RuP)b#;~@l`P`2_$*(z9%=Ps!v6s_X_2;BZJ-l7oyBJXpN zLg;D=hx2o9faV+v12!bIX|o#5H6<9#d<Qp49wg;)A?QUT1^1#Khn**?JH#yx`!FH@ zQYQSG))sq_(|ER@ObwZ~*?y0Cq#wY$k6A6sY{QM&56($(^BI&4cQpqFISb;(jiDT^ zFa0+eqEq=Wb;kBf1VL4ES&%f~Zs!sGUIoWF1{6vCkikMkT#icd2X`Vgv7gNQ4;suP z@J$pYFH7uhCb^#$cbbeIHOiF&+BO}n22vllCPGo#8%Cz_*aIQxt;Dr0gQrYhCmYH} z{km_w0%Tv*BmR=o8vN^VE!RJv6`oeA)#ZoN$#Ve=VBzADesL!|Oa7^cMpk?{6y)f2 ze1iQn82p*Yb<e39j1+(#1$6u1<izhOI@NW{h5b#<Sm)<;guT7ViICDKOr<|~sr!tC zJFd<daSy#K!9<i54L$3U_K(84sw-JzB1!`Qay;A+3naU;it5;T=l=;0w`vMClC|e- zmde{2-#u}K1V#_FSTegBcan8^6BlmUx4IH{(uFwt7Me@+aKTr$Vh8W=O|QHC&_$6+ zRh1{q5Mgon_55gV!JGJWMyGN*iewHF6tP)nVIi@?Ie3<7W-Dga1HeB}^7zul9uw7m z603)%TUo$qV(nOmDNPu?x7Z=Io!TYTA*EN@wgKqU6(;10Qbg;6(Rwm|X;Gfh7k;*O zN61F}Fqzmp&jwUW<*hi5Z-*=Q?2AUS8CJS5D&O54xaMU=W@3eoc>eB_6EIdn=1OBa zJAnZ?U*Sj0`MB6>R>iSALmld!{7I_`MnABNsZ9xGhwv)I48weE1a3~g*1L~((qPrw zl$NJ2*(;B}*@9Bo<&PSL|KKhz;kQb8?UX%93Ef+->TnK42@c1btr%qqZ(MR4)Q2+m zl!<jRn44VNcq)sJDbVeoD)LpUuy7<DDU8s9tIo)e0Jm4K$eL-!$bdJS%^R*kvT;^o zlh&O<qGB3tY4A)G$9dg9w=v`8FyXodh4y&z6f-LatCB&Lf_hnjo7z*w*ILPAo<~2< zI5-yzHCYE_3#-YtF2IkWR=K<71J1@~8Ae<Fr4lR3C5P;*unyU_5xf+O2SARL{d_3H z{3@Ay^PJeNhs0%gw)lx2Rz&xM5*dn`SG@%XV#AXbu}bV~jF^{W5<rpeId+R9;^T%A zxj6W-4br%xJY7)b*v`L-4giMPq<>ZfgL7G38nugu>bDfXE54e{I~L|BN{vhx-pPOw z&fn@2iGqDqfEm@P1GDCPRp~qu+dgtru0ZJ0_H*ZzAv&CSi(<64sS366ApYi#5w$Y4 zs$|$?2K=$t2j0uUD}akDf1#K&Z#`GKDsk^kbXV$0Fy4NJjkxqn-HM-<pP264=Sh!` zQWDKa1nvZVTNP%4Mb<k+^()XBjs2-4R-Sh;t<&s^(Ky=3O@C;4J9)A5k1-L5{)Z0x z21J1F{uLHLtwXoX;XgiXCuS({D`3}=7@hZ(aVl2UY?a2k^9aqFHbS(YZNk(Q1!wxt z8s3a9`>-J_4W_|hX}p6PmbN>DpD>iRx_3YJbw<x)%TL4Z6Hp5j%r1~T2Fh^LlL&QQ zlue2y`$p+x$Sw*CrdWVw)@5M_o!qC7QbbgnsRqL!ad~t!(HuO$6o0(BggjD#vY#VP z5Fb5Ob!9R_`gqcBITkb8DhF{)S+A_)bN=5*=tUZDi0vm~7XqXNYpQGQzNgy8XaDi1 zF5*D_3PQ>B@2o$n0BggL<(!P{qTxBvJ9zX^VCuDi75KV1mTfQJ&F<3e-kta$^vHfL zfRIWyM$r+AgEdKvkSH0^TGdt}nAK26Xny6-4C)jRIVLebkaP^ZEw43H>=Po7z704o zn}DV}h2~I`K2D>_LRRqRd`&j{@4D2|16!GqEpaj<Lq+F^{RW6RGp5L0bSy6Lre^Ly zGU@l%6EJ+Q{wzzTjWkvLl=FWu2M<Z)E<Gj2E~}&QyBGtjJ~zw1N3f5hP8CXQjdI~F zRX{*~V)uvDjz1a+UD{6F6l~fie7}EliW@Rd@4-jwxH6hy;MJTT>fur_vfU=Ns5&M# zJO+&2Isr;)+)74&5HpC#8N~kxn$!rhj6Ynr396tEl7WGJ%~W6j9Vv`P-Xq{vd?*U6 zbz-~i*r>B-1E>4OCc-{^?EQr*sSZt`4-7en7;8mG7j0tx#1oQ3;R?Vr8aWO&>b7tQ z##aed7h#)FrmN4Thu|7SGG@|la-xP4@VDU<qWG?0Lc{4K@@XsCu7hu>sK$xkDBD*; z3z9`fTu07AYW&&<84BPmvpAxXqPO4f&y!?C)mhq|b(d|v$xEF$k(d4Q0ApNo33GOx z&Xw}7Pd5_t?5%wdhZquadR7S0^7fBzf2heC6!kqxjsVx5K+h(36;d#{76?Qq1zY9T zNy#lU9}Pydl~p(uMnyd(Ie5^TKeSD_yts`SjE#@qbmYR&-wF~{2Q7LHO}?4tSNzGF zGvBr!;s{Pcu;yHSPEBJ7rZ~&;^%hBCnn)qQ$1`zzZTTV54Lo+^M#JL17yvo<QSR!_ zXYp%2D9yi=`RW(P8>I;*nSL~r+U6_E;rxEUo0u$s6tn($N{zLaulGF}#cT+$ZsBS% zJtaODwiO@0R=Ab#7@)PPs?HyA7B>1z-j&L}0bP|d9b+V*lt!r;n{1o)LMu@`1)gpF z&4~DHA_YhgHc(>dbJ1P+Na*U1NrY+i8CR_&KSxD_gHGlLlqsv8C4&~0gs4g*gOr)^ zn>Sfp1Ou}-S7*m-W3l5G!%{e`0)>!O(*F7K(3#%xo_cKbIQ!Lf@0JR5yjkP#5n0Ji zk2&qpLze6<o3b1QBoMSx1J>AbqEU_Os#wTAcOs_K$+L!+$b0nL?^QYD(R|@C?3^70 z|G!9IHh-&WakNl!7xqy&9~h7oe!e-KzOFmNl7>FAj8t%qQ7{be5+@l+rkeVS#5x+4 zsw#G|$n9g~6_(tH3%pfun`KkJb5NgOy18^_`U%a<?%huM5$7#7b*jPSKeQ@JA9)U6 z)|B&c48S##HqZFGPQwwYQFut%o%VCswT9XB?G9(73dr-5b%)pw6f9yB<|vu!^#!mC z2#L4z?Hzdk0$z)W?G-$yG>Y47l{`F)*kYMGegz)%TUNkn9NPgu;^YQA#P7Z(866Af z#)d74o0=-4SOuRoB#%GI@11cqsFtx9QlR<&L^Og~F1p5bV_ZE-c)i{#4QJ+j;5si6 z%*V*Od81;7_;6X5D4Pfs(qFxOvnFYGW0UtJeMUNRS`<FCNC8Uc2I7OLkD{(?zO}0I zvm9K%%$I(7>vUdT%B#S@px)RLJK83vNAVkUs~h5_?Fm+o1-4J`VrNS=(@5ekmVS%z zWteIQsZ18mJ5xQ_0UCC~DY+z^7{3UA3P-7atA-OQ0!8QTj#9p>8)R)s#sP(d=!0ia za=&{SLLW`#E|LEuItoWaN-oCXAKz=VkUe$?1xXL>T(92I=%qdJ%@8^B;tOp#FkO!P zhwe$n=4@1Xv!SJ_D<O=r96*_){Hdxe04d?yz!TkOyT4_)!~5hT026Jq&{gHn3KF8c z9y2_A4{r{y2R(YVl$3!Q3ruOC0;Q~oRs>1Ep2QNqr@1<o;(p`A#U(yS-KWiD8DZmx z9{OLlF4gC>W=Yr$-JUGk5Qq8KSnaS6f(5Fx+X-|TZJQqAJ1@ZxWM`Q&M}{~f6noJx z4jHtQDYgUkvkgW=1!@f+0j79nW-m!gn`v|Tr)msT&+>c^G{UP_ama_aL!8N(PSmBg zN+(W*X%_~s6<f(>H}FJZ@CKm+)*qtzO;NTzjO?savRWrC?UyCB#+7n44S0>s^l5XV zXimlq64R4pTpaUtcQ`n*lDk4%KX4B4Ry?YU#ifHbGlTwwY7QJ89%2`)A8Sla4sJCD z0lod80~7Peu||>_!y0SQm`3gT8EOkJMd4J@gK&V(0D-jP`C{m6Y?R`4R^QdJ1@IWV z8>y<LfRb>b)_~ol9VuN*)8x3#;IwU?l8TZS*OK|k^q+X;#JVzK!LBTq36@kX50ud; zW$c$E-V}_iV2?qXnI*INh$AyD&3tT(=b7edDuAwIp5-)2yS3w50SnXGu&wFQvTlZg z3@l?c>aRQQ1;iH<GMyHuec-Jim{dyYTzVC)*)I*PaFS*w5h%ykO=3)1w+Mxyl<#Ck z^k(8~nLYWCaf7m7rp98~x_!kv%Z>l<rEy`6O5z;@4+L_4Y5?!Rwp92x3syfbO0k>^ zJ>M0w#X;5stUgfU^qKG(V}O0gsAkiON}!Xv0N{eu1(gt)V4<7?C;(i!igi!?tdyuB zIetDOXp_L(J*8JXz=jpicJ5uNX@o=p6@>1qWa~t8(`hjjl~!d*QE33<dwPhN@r&;4 zi{5NBzE{XddxMb^OB)SwN#d&|5G*6I@=-@1(y4{L4e7TN9rsWXnsniQqZq5L0+Ru~ zmq!_6B%=}z;)LSuhjoJ22ao@a{ekam^<QvR+DS^paD{x*UdR}O57+|nc4}WhgA?V3 zme%l6@G$FIt!^QD10(&59~f|WeMVu4Ah+vM>U<}B15Vh9;=#!6tuY1`xyKYeFJQ83 zVTZqs)$k;A0#owPXb>hR7oiRRXY)PEGy1896D#SnZSPP;MpwS~NJ4JAUJ1VK;S6%1 zRoaXl6Wt;kQ*m`J`Tu%@)#`JebIL-T?r&bExnjmw?_u@%*E-Cbz4(uKLkSrolhT8V z6+a*6xUR2ZuVlCM1|{}KUxt0=cP|*CeI`X2%=rFgrgYk^ZA6nezS_5@X!}2J5<sDd znINcZ7!GowEM2=BWsI57nXX$VzbXi*j>7ryNPb%R`@0fI9thkj8fw>F0)2K2+J&Rc z|G(sdjZ8n<pED?E|8adGry%MrrqbvzoV4j;Ls8?Pq?|a0iIWRu2N#Rf`C0?WHQjD* z@7;wm;05t2e+|Egl%U*iPlB`Rq*=$!X%?QOQJb?D);$Qa3`^g<EEz90rPwy-Y}}ro zVlW}rN4ntOM6#jlVOUkbHz8cBL^IStp2_>atA#`d43b37($0^KA*BFFNEMhdC6b*i z)|2Pti9*N9NC9no0B?wPP8(6hV~ecTYhPP~7vIF5qcx;+Q>}S+_aSgRCdZF92-j}H z!1Tca0WV^Ob41B&%u-gEW$Sanb%Vtj2j8;*=UMWIPC}M;)%5(Sat?_#qt&ZDAN3Fz z7qyUKX}P`cd+JG0wZfY|8RG#_7}c3?RZP`5!Z{QQKdFd9BM5WQEH=b4<RY<#v(5Iu z8U06;%c)@;K9L4>#<%<P7%@Rr3&)<X%z_+ZhGB0kVJFRpCb}az+lvLDFoi4Hv$Bfx zdMTBPXI%RpIkBD55x&qZ&hEl+U&!d&f*L=W-Z|I2|M;Kaz$-w{SaMoX9pM}ekx?UF zE_!pjLTms+5Ja*UeSN`ezM?k9=%YjmwezM?yqlg-s6T`*J3(*0xdvod+CBnm!X}sr zp$OzJ0(H_tBJ&6D))93AfT`cVk1|iC#0cH*4L*l@RUIq3`M^**-?!^NE}5My3#!jh z0=wl8J~2&}`oj0>hzey@>7-4kOgrwy=E-xLWt0kZr#<pjPcBDL5&PGbTV}Vo0LNXU zBRd~8CGr9h_NPbZ92r}TfjPWItPBv;L{=>%k(n{lcFsp!zxzA+HN%yF1`T8nZvbpO zx^74K2?-KgUnvU2KmnBS&3%BXz?5)wm*sb*cqxo_$UQI|304-y3%20vw(QlHAi>r> zc(Su|Q%P9+Fj<Lr|J3+BLAf7X(Rq{JBK_SUz)<`kJ<0Qc82MsuAKo<0{zv9osy|BB zq2LRKVc;y!WjB5TRF##VvE<&`Mt|ZKFZ9PWBG^9H2^lJ)v)}j>Gnv*{wd}DkUxRW2 zueGc%N88#5C2di<5aK5EKK5jf7gW`Y+iX2Q0W|F$5}o%K{{KHuWKII!$+Fx{+;ceH zU*sAWZ0e{qN7MyL!E_6t<V$KGs{HV&3o4vk5LbqTfhpeg5IS@k?4)5dn_!DAWco&Y zoA4R`(nC_1Wgqmx@V5tHJ8!u54qeUTdVZRD7D32=tA?DOQ$v|v1Wd4nYjzfS0Qjxc zoW=&DvK(xT7}Li|iscBN4d6Ud))F-vEKjThi8cg8A2l*<GUoxxTn<1w)@OOTcYU+E z$pM03v7CfdJl*L%7chb{+}fv{TB#rV^z&ZHwwg%)XQJU*wM<01%pD}S^qmLDSLn>3 zJGGh1wb~O&57S*}tKix1JAz|#1CmkBn!s;_gR3N>|0u>pdfau2nqZ*s=EdHGA$i%p z@(ucZ6Ne&`U9oUAIE*io!DiOn8?ZZZybo}!vf~6(Yq<LLKE|)3&Go<tKBKu}>GUxl z3DN~C=1*ft2#HzVf6Rz9pZ3v9aCBmt4C?}uElDC>Pox{<2;|b`?YK(T^@yVQ2t}L# z_g~LeeYt?Z>pqjniFzs8d>?vYyk@=}@V6eo=tg9?!Us*WkkPqMBiebD8HQV*&G0PQ z7ei)<WHw=?7I*6Z<0j3?naRZ^Qu<sAQ81eayjF&o)dDqWOBJ^=2FeDc;MpWDMF0&~ zdWDxVIX@9d2@MFLEp0UIz%~l3=q<Q$w-1!Up&yw5O(o^l6iKi8qu3e17(aGyQ|TV1 zqrs<qM~eXn=w$jrbZSH#!U16K3Q^k!w_1S-G^;$31Xl-o6}q<7zZBN+b)77d{EKp& zv|>C34efqTgQh~B4+yI`4sgJB6d5b1jG%NK3KpxYbQ=+UL<}aEuP+S6+={t&B@GPn zg=y(y0*P4a<Z@>qjPFV{{AX$~_wK>x7PXE8?V;>OQ|SqBb#4RAsOP_i@+yuKVW^{X z>qJ@AMbAJvLaBe5FwVg(!pflb3AAb>+37m0w}sb;$wITw)6&+ERfJzF)*NXVU>KYj zo4C=o9OojyQ@`F8YFm#h0`&EykB}v&O5#<5!0NFCIwYb@-@>0aw@wGr@`jN#k!!zx zF3UZDe3*EzPsRI@0rGjHfMewLUkL&p4+<C;DE-+7Rva=Y52vemExBNAca_qzkoh|E zH{PxK_mylr5L!t8Tt*1D3<}Rj`V*|Nc9z9HcDQhY`MKWj>Sdq=&#&)~7#OGj(IAuJ zJ?)+k1F1}qLPR~!^hd}PJY#Vpp@`y9%1iv8#DoJ`@G<lip_biOW@xwbh(T7An`hd# z5GoT|ZmwWlMSmV%I8y>-k6Fy*G8Ry9FD=<`K_;7#BG!NC3w~JMQK_=?5C3PZ6YbrG z%8Rs{X)QR!M$(y-GEpu5*PFBsf+1wDUK`f5hAaX0vCMBk4sE+$DVun?1{}4Xdw&tw z?!>;C<u5U}3&9IKDf(3seB%!I#F=*YWb1Rj7|(9{=5zjqiCANp0C`zg$raZR!aay2 z2j4?@js?{qMD3SzK@R?*6_>*ZOseG+Gh}aW-5DEhqv!R<7_}8L8gBVO{My<1DbYiA zE82rJJgU-$c4wQ5=zMuK1nqZ`i><6t+5fE?{Lw#!1_=7Ocl5%Z7$4lNaFS`PW7|D4 z^H!`{C1Q>nafYP~W_5VC)}L(pxA@UZ&wsyQ8S>U|$L*E_{}K*3lF9zRLHuf`c3)GY zlb;=ERaMbYK1mw|vQqvA&{-Eq8LGm~0>EBgPJrj&#>mmy9(6=O2Bah&i$h2H=RES< z{ALaTKLKf?xet~E?EX^m#84q=8MukBrFCY#Y6vw*rV7NA%{lLMDMT2^J2ZxhXuaJ> zjmC9)aipI3E3}}aA3vi?1=a?lSU^_|&0v6a3Ubd6m4+o`0){U@o`?ZuPCN*~$#D!o zh>UV2t~c2e<hf{H2WyZB;6-ZOfhmZK?0=iUBLHPoDF%RLMMi5y9*2T*?-@wTV8bsE z8^9->>UKaA5!=m-tO~@jaK*KP{mOSkt~ByU>wEFIeMJ_eNcd6t%c(Yed~RoSbgjvI z9ytCo0xE9J*1&AnZ1bg_C<gVDtnz94BkAsOh^w}z#=uHt7)z>Af1|uCIna?-#J(`4 z3^Du22F(WfE3FbpXSwX@NJxv#zJOa@h_aoYmL`l~Mr-Lfq=>Y`L|3{g8==&^02@9& zcM<UZyuH_Qgld%7Uz2vA>!jk_L6(lC;QFZXyn0L3aWl}un-O^W@|0N@7z8&#B|*Gt zY`aCBv>(>#{82)7mYusVIv^}gs3LGh9_XVU)}Zuhx85d`fm6BX4893QsKdE-K}a<q z*c!NYTjIg$+Nb^xkYjII5)!A3t($4C#pU`YNL!Y4WK;P07KY;c31cshQ{-IhVda3< za2*mC$`1sLS8z+B6M8AT)~@J0fMV`3&damC`&9nL6zB6?sbD&HTNX$M*(U{3mMOBA z5a`^87XGDxe#bu6!V5-+$p(mCKJyP;b!8}j7SD&%rG)CM<YlH)SYur8VdK|Sn-$gE zYYkaIhvV-c*0T5;Bh=cm5=CPT7}<*P5VoPmXqF&82*VN7)l_4R5EqSSTGKyjf%q<< zZYiSn-*Q^^#KBY+tsTy6h0lLM0>Li)OOragUXs(rEg!`TQg2J@d3owxgB*~PA1khV zsWghS+%M(G3uqc%kIlmF4wJC4Y>A+G<AMr?Uvw)8PM#_O>1H)G(1F+nZ1Pnx<4llO z8))<2cGRgO!X4ng3!IRe#!#JUfD;-*7ehMG(i+%G^9`Jxf7|v<N@w&CJf8)R8!?;{ z6j8UU-RkwF54!0qLPkx%>pa>JbB+K-)Qr{*j)-cJWu*E}lRFV3)*GRgIX3R0xgC?H zHxmn533Ph-K~c2G;5b<)C<)B)4Rl_PLu3+XX(&(viKP6m6+L$;<8|;iPOfHmESCh{ z0+@(P(ViE9+KJv3*IH28v<@nEZpMze=mUc@pEFJxMQWPyF5$E*_3Q6rmYRhl6{frL z2<fu`fhK}+tBYV7>~y5xK;%VkxaS*(3fqQ)R6&YWnL&WF_h!U*idF8i1(M5ydg4}w z$O2Ujv&0M5Nf75Ftl0hOZ7xNtGL?cd!=tEI$;5lhdVY)w&DW^^3dE04(8l^!e6ZPF zF_N^U#nngabc?Dkx-7D0r*Se&iYY@fMN(IF{HJn14>U)#6QKLPcg%zbCu1;%ITows zhl~xhr%`jP_=j;Y`ujDXztOK^<vdcj!14R%63YFr3oO;Yuei=BgqXmFGqGJ5Zqd2% z&BKv~j267dXf+N}&e6RrM__y`(^BQU0BgEc36zamP!2P$DwSOTG-YYO7}F}@xShk< z@;`O}suEvwHga7gPnoi$D(g}JDmMhG2}8-@-xu}jfKOYc=h$SJP>vm5Yvh`!pMUMy zSX|v3Auy~Vk&;?`j2zWl?>={E87Ve{d()!8$k)i3OI_&|=T(2OS)bOPzA~kZ(NbgP zw#K1Dma}hYYBB}IwKB571hu-tGH%A`harkKy$L`am(5@E;lTtpCMindI=j6dgp}&) zF}ADh@}U?ql+a*U3a!Z_+RlA0DAWOOS4p);p9|qZm>r%H>$9Yzedj_BMSi`oEpXle zgu0#kc++t{1{38;Njo2s1j<Dwtxl){YwR<6duA)eSF}mRHCx1P7OJ9dVN%j8j~^CO zPl3t15~$CV`gn+~Nw$(+SNIlSM(YQc??c`j*1+$1F0e?99&sw!-q<L~@Pdulp?+S> z8Nn+aS~T+py*a}I#(m2Ht*7<i%(vaqtlmhGZPUo-qouaI=Chy)cd-_e{FD{&8PkZh zj}?n#Ral0~KP^fN6&|InIkZEWlmL+y_qA%xGpL>XvFn3VpX22Zk0?fL12e<UxBYNR zR55R$cNIp)tX>SaO(gtwqj>)NtAs<!Qdis{M;fbB{90djHMD_q3Go;gXY<~Z{|UOk zYYRlBf#Cu(?J%j1Ksr48t;t=SO~Dkliny`zJM@^9ah~Tj7jdKw#7>5qqMnt;dct48 zqXwu8Hy;OV^csnzC@e5}Lw&`XRc*>fFCQMx&8M@+62E^46LR{sgI8FR(CjwgoCusz zRUtgNeF&Jp75noA3c_{vpRG46y`&%s&m*%c4B%}1*Aq%TAv?n?M0hisLU+4jz%%fT z_G4iaCniW$Sapg%K|-GBYi}S?@(I{_1ru@TbNhhoL<$_Rb+H@hx-D}Jjrc~Z(FJ*p zwpzV(qkC^*&;$INC>A&g)!AUn4R|Z0MemBE3ohqpIse4(+O9L!Tf2vZfQI4%t2gen z*0s<s`)}9Av4V_w(<qS`7?(8fpH6iwYmEqIId8u70MC}lu4%hyvU$3UA2lcXb~{MJ zn9Oy?ly77G=E)oQ7@F(ZQSe2VP~s4@875%ewb{ebFQ5!_O|{8gT=BQu4y<~UhUB54 zTd98!agG|;7j;5fE1kzRg&Ne%H6CfToJ0x3{CI>7$-*IB1EEVBExymbQ4+2M9I)rH zFG^Q10Bbgmf>PW%>G$;tBBp5J#47`k^(g-xCwewPPm<o)Rt5?-af#EV=4etK*wOxC z4h9<90HfU<jPd+Ls&SGvvUA;qL}za7t2t$`w&7rbFQ0s4{Bnh?q)cP3sqiDH3ZH+q zim8mi2%fw(SeAeSz=^$$rq4-9PYqLN$bGUEe(Zb&wi{sj6^uV&qfYT_42ru=L!*AJ zbeJ(>%%`-A?A^-S=`G=r_<}TZ4X1EVnm2cph`E+<L}NygUBF%`6;oPCcD2A*EiDin zsM54yhPDU%5y5cb1lF<V?^6e7x*QsrOf}gMYi4b>8HeEL65bjjGWrJ=6LxDzNnU16 zFqkYRRYB7D)M0DRNZ^d7c>F_vK7sO16S=A7n|f~p09pD7NPa?qxZRNyY(E|26?8F` zVMzOVCJQ4VTBl$*WYcmn1-uM{Ie45}2d*Mn7;7+JxqP<yyDJX5&TFo(oNkZ(F<F7F z?cg-CP&4Tg8Q(*swU_j;2EE^?YhHko2cH2+M%7&)&-$?hoMuzDk>+nOIP9+Nzj=OG z3%0yV9OF1Y0XVqoXI?zx+Rz#e1g<m^Er3df6wyRVFl}u+sh9yxw<%1-K6K3Ax8eO* z4NmS!KR@(o$@pj4h{`T)5sd@&VQ^B;6X{M0q)`0DaGU1+uZ7V#{po21M?#`S#qKQ| zC^#gQ8tcs2<YzcX57CRwmHa$EdOn!!`a5=nQydvq{{%e551M2U0Razy8-%bwUoZV} z+JkJL6z#a-6c9{ayf52`u50Gf%C*&fG+!Cnnlduh_qVceV0QQYIw6<=+QBq4^zbzG zez*4<8GJ}9ac&E^&D?Q#j>g=`spYTQ3XdT!MM_RFGp%s(nf~<r>+B_I@$b9>q=X{x z5ZPU#PvnluaSZIDBAWxtAXKB-cTQRtA36<;=*C(88$b7BRnWkeNz87-lv<^`2G8U= zbF}9OGJZfXEGA&;5p~4}O?Xe8y`hZmQm2O-jE{5N``Bfm6Nd~v-78xS29+t@kOoN{ z?H3k@YK<b{7Bj3D3h1K1lk0`f=pw?Y!t**kj6+mkRJL0{{BRQ01KW*<_pB9+yXM{3 z;o%Y};ZHmFuKh>p_3n5mT?4XucOL^x!S?3PR69kCNBDsG5jEa$s!6$|eg4$DcHjM; zsQ?jkDu*pH|46f`-$uaAE>5QX5jhUwQ=VB;zmph)4s;t5{%QKD&9x)zgln|rw(h`= zpirA&A#0zk>|!7c{H06tju`8S2-%$N6egH{0x3YxhjT{#$WX_e%LMtP1#2w2?sJ>P zw!)4w#A`Io_}!ZXOwM;)%nobn*+I-$2@>5Z77_!+p$@4x8Yu$=Qu~WMWYYhM0osck zJB3wc{SxgcvdLTJabKILg>&p}4kUr3BUI(=MXdC9B0!t!%d?d2nA1Bk_=L*&ja2-V zxW8X^)ZH%k-W=LmSj{km0{TT<k{xY(ah2nS85vqT65%hf5S}cd2)7VD@j;O4(k<TM z1mpE5P=dp4X@k)44_1L!D<D_^qV^5VkWY2c&O$jnxUW>IpOi68i(ykNQoG#C1pxF( z>R(c$5Q^S#4^vs&F>E>mk)6xOosX&oJ4cZO`ov)n3fU$;ix-%GMR-^qcJ!QnT#5$s zB{Ree1=m{IYp!Z&yF)loP|rVpJ*u0+|FQ2RS&Lydf<%TLMQ<+`5|D`0^J;B<_Anr{ z6-C!<+OsS>(0z&QvF(d~s_|{gHl56EooTpk2<P~~y59-U{#*fbFQw3S<buzI5;G~h zqh%}HCo5etKAkmE)SR_2qg0}EN{*6|Vb51h`R5=+m-ncSTrGcA65fdQ2;8i9BN_O? z1DZ5de;%;4r7Z~~zK0M?xYt7r#%R|&^tIzuyb&x0qr$z!NlK}n3^BzwRz(YDoWZgw z@Pk7X5DYS`R-J;)^Xd~g8De?;BE8yPHm4D<kNnhC41WK83e(GrypXuPzSw3rRNlTE zA6>^S-mae}pD0&8eV)nhl~4~w4(%@!ssxhtj*_dR1o(L|x5LI2(nN)_fBmEx-3}P0 z$wo64)3QK{v$c#IC!<{>eP%!EW5NED5V&0(3JO67l75u3+#?W9>frukiB<e5a`oFU zL^3mly9iR|^9T0P=x1EL4$8idlY`p&7VmE(dT~4|4}@8xmve^)*w1(}ND!sw%qDM9 z;qe}6MJu{!|6X*E3$vqT{nf3U1E+R)Scw!5EmUIMGVGqvoPJD16Byj*tg5?^g0cDs za&HMXG5!~i5&tn~d3ES208)+OF?5ouvg&J47~7XoTFLfV8=gWsb}L`^_F<-Q$>m5d zwUAauu^RI!fDOsI3MY-`dwM)y%Cq9+YbOGDV_w{8pm|SmL|ozJ4Z$_buv&TLyI}_? zlznLOn|rt;63~dO>t(M2?{&ssqDDvpmy{c*?RVT$*dA^M60lOV3;ptp@F(cd?A)P? zq$^9X0=tS2tMaQd_7_(kxk&rV#n@*Be_A>mM@w6vNn05j+yD;w%xqI6))X58==nJn z6jRg!73Q~+9IQFCrjhL<T(*ZMmWBB(;U>J)nf3x8*Eo}Dw+4U(UMBAyd4%tw1&%No z2V<LdaS?EBZua*^{cqVt9@E9?RF<S`>h_UrUR|RzX_f({-2jW=U8rkz47W_(>f>Vv zP*%{79g_}bwi%;D+pt7SFvie0f4f{91nkc4?NTNn00=SPF0o*t53y=(=v!tGh#|tH zT<uJ165AF~a;yD8ev!Cb2Jrf(0M4jTs|1>9R4Z8NjD>J;0><sd$>L?m^G3MgS<B;E zqX}(;i3Jt_#@J?YH=K-RXu%L&_c0tl9?fwQ2I@qr2-1sK$Pq&Q6^Ly&4;Q%n8T*b{ z0zjypMSiI;gobsl4(<sPLTZzsn1pqC2hpjN3L}{Fc`F<Ex$CCIKls?fmLg-$at19F z|Av&f#EnNty*m5ZCuB&ImwA^y#?^(+63l15?-C&;ZegK7(}eCRBB+re`4m789Haz7 z(&PUjI`H+zF^D-V!j5kLcDQa+3F$t6Ti`R&l=mJJ{N%@b4?qZG;*F*OUQqt>y<H1F z(6Q-oKrX3W{kk*~WZ=Wp0s4u@f{nu6Vys%#q$S6luycnw7p2np@bp)7O*W?S;om*p zpZ9Y*%YadwY*Oy-7xVLAeN?(qIxoi@-3kQTjBZH-{;9_nQ)u0)`al&ERSJ=dnb2eM zg+I0omL9To3o~%(PlE|%-tc1kT!cr7&&ezDqa^zDowe8brdf%c%7dtrCO$Ip`6S4_ zRFHfr4E)5W%m1xFAEP%3CRxt9{5B``b881gmAsu_scRyAMDir)Q|I$96&SludR949 z6u=$Q^%GlXLQ1dn$Ndo$zQloyeQMv6#rRS^oQpsY_dU_}vH&s%)|N}Lt#2s03W(FO zl4^BzNMWPQd8HkHe7$q+Fzf_CRiQ_A8+FSneNVbu5ZH{-+qNtik~i-`4y-Y$fY@dq z^MUq|=mqP%Ru0d>eqp^x+?LIlw?<?~_HV?!>XmKz7}vdJEK9|N0=rUq*PMO<q5>5? zBR=k}f5>aco5DeyXBE=+KgAYJp!9oCGjB?Ww}8b>hQZE51n17_YFp(Xjh9>V0D_Pj zbyg4)KJs#FUcMBji5L&~ovJ?%6y5V)&(c%RC3v$A65_Xnk)5!bkq2<=Q*s#4U-0_i z;(zAyVlv)kREHp=y_eDoL#v$%1{)%Kusy;C7(Hb1-zBLu+ff9N9UTiO-Rd?>S3>a& z7Ao_sY!Sz)6>VDunXVj`Q>t-|!1hnE2xOmH?c(RromLiDZ|m$O9l3Rhn|Fy#MYek; zB4pG)4Yzf5&O!7Jti{*PD<SOL3AIi*B<(0V4HgVHD{>6ADk942c#Pe}^cE|nyE~Rk zi!RgLG~3%~nKBqfCJZlI4WZ&Uu1M;J<ZPt&lWzT`B_3xzK8yk0%au0O+3_-_J&y-` zmyjsVfVa&Oj9u*e5W7Bgocu+@PlAxJ;3p{WdP^UzG2lv?kwQ*{^yt>UV?iqqQB!sx z>*cj9KS9do6DU$&i+I}ylU?xB0^oLYtYZF4TGFYmzU`|axZaE%L>Qvm+XItqJUErH zozQl30LWWI34Kj6*kNU;6M=C8h2aFFeS`?8JNbB%$FJ<jH0U0#E?MtAWYDHT%CV7p z6A^c~e1}%O#Z3!h#%=@gb*w+DU7Di(Sv`K#2{rOSN&@_<P$Qe>0Ryl`R0Vw=3zs<i zY^LJ$qn^kQoZ9dE{HXGs*zjPkZIZYcVHomvR!%3-@_w%1`D8IRljV{a5fZbIV~*4& zBUJJam)CNiNJ!)3B-%4B7xH3}B77{{8nL9i1|tuo2Oqw9)9*Id1Lu&H-4hxeIFs(N zcX+pnXC(hjmPs3hFW25hPc!m;vhX!b{E09mrr{UQL^)u#2E8A6D%nFqO;GR5@R=eN z=!45MTYAZBghh-mEQ>S>PeAQYZ3ZTbQr~#z4c5xw0W+kTbw4RE6k(pw>il>^vQhTk zv0!{${?WVbS>xslj5X|luPY7-!A3*KgN<$b0=#qAGq)@q0msCWL*&@R-(l^7#+rHx zAu`wcc>5h$gW?L+t4~j!B#Er8?7<hY3`YdA$C<b7cgrmqBPdu6>r9a?++sF0>ZHBv z+_7YuBFT-tjq1N-OuuOBGR8vH04T``!A_HTtug0TMJ%L7PWk^j1CaX>`>*N(CP9MC zrTjeTBdRPFUy4?C>VDo|44CA728d}Doq@nSH;ELoG?-W6qXa754oUz>N0`_DD<^dQ z6g=(S$e9Or1=j>r5r`lPFVM>lRTiMZ<W6O}Z4pc9W}~4+YO5BLkZ{aYL-45$bk0TC z@rCdkp3A7F21E=WwjJ;CF}L9sBN`YQzggo#@%Z?m$ufMc{TUq&>jI}PvTu<Xui`TE z+ieuY6tVnjZ5`$amA{GTzprk$NX!0U=TnaP6y+TqmL@j;{pI<kZTTZG&zaGPUryLm z2Ap~NO1y<V8TRrCnS@>HR1`)PbKNiv!*t5?L-O|;>x%yP`&JZg+z%p$W)Bp@5;{X< ztkSETLgA9U>p~{`kVVDhw^16WVvlBPmDzmgPlVb5Jyz#K>ZDoMb(Sja6Q!&`n2V}F z0@4Js2mTs_pcI%n#Mg=_7I&>h7D|7v0AW*JJpvJ4V^Efv6{1|G2?w9U%g6`8GNx;# zF@ko+CHl5~VexFQKXaw}loK8pD`d&W0AS*h$!#COc9*(l27>LtLs+tuT^tg9t<otA zU!@EGB3s$Ds%<&u3Y6dEQarP{nImxi7KYOX<nCK42-Ici9iBPesVrnxV0?BBhplA8 z;P-iBGvS64bJwNF*Gj#gQ*O^Jes#20`%zan59}OZAtQOd{eAKAunJVq0McLd@K|Wk zta9JO7SG;A1H@Dy>lJJWp9T|2ap7{_5Gs$VZ_gyQj+aeRnCph;rAOkqwJ!?P4u#a# z$?r5{jVci%RX8pTGbn`}Rp4-;4aNM~y(2d(U9N|ePhC&#E^8U?Eob-zEZ?{hi{3+p zu`(neHIE3qdN=qG&Z~E*5v9<Usds2uUT;*GnJ_cstaAFSbb&;HQv!ZnjMr$qielu8 z5>_4z>(Fhsn2@o+6I$MJ!<*fwkj|}1fRcz3r5na1bcii*85~t2Ev<e=!Ecz0x9ap$ zW>?p%&#+;d35|AIhU$VelfkVKyUJ7=yxoWgOwR16?LSEAJtk69{#|RuH6YhNUQa1S zgJ;(G0-2Tjko=83(NfBS@|i-j7#2hYcnGV=3uA*UQJN=qY|`>Fdukm>yJ$?W(szhh z29aBA$SYl)JOuB5|3kTR*ZK3ql~ZIc_=|cz1b%im+D`HjZm@(Atd;Tt_U<LQ6HzX% zmCmdddEt!pl6P09U1+`bYl%94W_}XoX3bx^1HEK@IcFXzN90@Q-LL}Q3OPGeo5vSx z1N<FfO6UP7PN*`im}5E;HpNxe2R-s?6~O&JQy#P7WEB%;UqC6a1qYYF0P1bHdFOzh zRB-ECv--<5bIZwCey<bh?OElU5CmXXM*y$!jK?UEd~H~n4sgt0RZF#Z6jHFk`75)l zrh~%yE!R7oA?tVgvZ)*5JICip%o&UowNBUauCl0h3{}dqB45|*ALV4fL28=yN6|7k zSIBUpJxrlJim~5U@w3DRtRcb!;{@6a2+{0R7fwlE)pWr<N&1e&Dm}+7_LKMS_n)0Y zQ`|dOnW0QVggVQ6i#!)7JKEEZIO(lj38<)MPbZmW3xj(0a~*y-RU|@xqM-zL^!v;+ z%YuFj190Y6slOBU5u#PPkLIXg7HhmBGO7b0Dp!>;PV0U{N7ng8Qeo>ZO(VVp!JRb; zKn$*S@ygaOz?D(GkcFPy6G*!M>2PAm2fvW4-<9$4UNYd+Gm*E;VD8z9b%184#nI4> z=2uXg&={j3ug_%-50H4gz)946{QE)JHkdmV_XmHC%fgR+C)eW~nQGgWr`k@3>Bx?k zj<iXu+Sww*5I-JZq$Z~xOY3XK+;Vjp;E%SlUi|PxZaQxI>yC#<3)x#AxM$kz9e=U7 zmi%8i76)qLd#sF^7vgRZASc}olk1ABYA<_Z+E(U>*&Wx28_QuDuOpHhKq$a;LZev9 z0+ccocLZI5Q7!?tDoNET*uT|!Ce7z^y<e>$NDjW5lR->*nGO&QR9e`3n_B8wZlzE) ze-&(pkSp;-T_Erl^BO0>5!0tyVe)Rm#YAA`dA1EbK`;}wi&Kza*uwJw>sc#yDi}qB zKN{e)OKAnCqA6ywEkNsMTXR2COqi<Tm1Kievd|bR*);kZ_{s{8-K1uh1_;6?2?#~b zI#F&mW&B7u3hfr^Orv;u@5Bo>F~d)V_miZ1x2%frt}oH5etEeM;ue%cfb9k16Pwyo z5>*#{UKXE%fSnDWg*q9vGt=-;P;U}5D}^ebl~J!;z?e(xnGV$Zy}bz$8{XgwvhC~r ztsy86pZ=@72M5141xGFOiIN7*CThZ43f?BOEC&=9fCp+!`7-6bupXf~PqreC)y)Ud z^=X=|;@gShCHGt+LD$KIQ4lF!Yc8GCJ4RRH@eH<N_o~t;1jj?`FEG&HpfT=zh}m`# zXCWN=aLd?~+i7)c*pYygK*9Qe)SyYG))^gMdmgJNQbcxa27YZd#zEq_i8Ixq%`Ve6 z5JKI~%QMduQ}$ltSFRm+PCIzjK^E`_3)b@_#OtbV5H-i0c{3mN87Qt|jEZJQ+0QL* zX~pG|G_~am|9L^_mluHi_!IWOrI#!)mnnH+VTB@&q(k1I+3KT0B|3p>funY43Z)7M z<@%TGN+w>x@gj+s;Q&34ItN|=+!@2#>T;8^`^o-?PcZj&Vu3jBF!#Y%<f~V)p7G=S zyc(LO3jP=q&=l?7J2KyXzX|RIJ?z*5m1*{UxpmRwsZ&@N2ko24S3=3Dn!~5dF_jAV z2ZtX`gbVb<i1uN!*D=vHb#|0>UJTI~B3anykjbs03w~8l+i8{8{z~)FAEb{2sI^${ zstw!-^G9d~r)HYqjV3x_p+^qt10<!=XDx81mKmG~G8uEY&%Z@~p|<Yjum$4Xh!a$Q zgCu~|iD_8<XH<n@63D)@1C34r<Sb;&xVaE4irwNZ&3`h~Dl2k?<f~r7a0;VVI;59S z-JpZ0S-!aRMT<F6!G$Kq4$!k2xOHicgrM{L3s7FtIzrEYT}imi84TcEN)!aRj~Q<g zwj8Ay=T64Y4Py2#e3zP|43L12yqB5dVp@t=Q;xly^`y%5`vsboOL9b_u7>K0h*&h% z%b&rm#_IWbnPG@FTh!lh8P59e>glM;t=NzP3R_20KNLY5LpuW<BPSgn4LVRjuqwUQ zq2;k)d$eu(QaBL<2*r^w@A*qz*3%>&_gXju8UW4_+^c)u7_#HdE#k`C{^tx0graIG zZu8TN*tlY70)VpO_$g`r{iJJ@id}nT5dc~4Y8l)d7H%=h?z&fTN^R)&+jHjYZCoth zcApdSqLdA{zWXD}ACkzC2Qd-j3=<tSSK#IYoSEHAu49mI!P$}?6WvI83OeXOimOxf zq&61LfIexBAetRo9k)u?D+^^;tt^&$H)v5f<m;vfm5O?R90IzHe0YM?RepDVt3#~0 zxqO^}To94RK49oN@fb>W8N9Y#NEgyVR<Lry#lY1!1<Ve8F<1+%2tt5fGA)s@`%8wc z9*A?pF0hy&^%KlkGv~*rdb=~3i(+TgeK+6<XVVNonq06U?LX%U^zL*y-V1D&SnfUS zuKlK@E(pXHKagM}-Ko}v8L?XI2I##oNAqfRFeZOUCB(t#uJBBqu6xl87-VpTJg^#) z2@IfLE=Jg;%R^dk_UMP~hk=Q<{+weB9H9$CLuP|<{qfc4yX;?c>_m?}lU$%<0{~T} z{+?(9t-%Fa#cT<ac({09690UX5gsH{jq(-SjY@#+@^(!u<al=QQ&$W2#S}G>uf<qd zmxrbLH8z)+9K-AR%5y=eA|WUJZIx_{pC+=H(47ldV5<bgCAIoXTLwQW2*%E%OE*so zmqSljk7NvCEBd=f<XOm6C2m;c7D|~0{#=}1Uq#u74X)ehMhq0iA=EN@#v>DLwtr@U z;)kv9xa%eYgbdwzPpxWJTWo%v%C+Pfw_#hNF0Jh<<P*-qb_mW9tYW)D4m~|vNB6h$ zD`ADS(+Q;qD!_ha;@<zcLEbF0%3Bgi`%VBbg%y!dQhBC8W<1(K8#{Sl6bl#K;|;@e z#-Z%p3pp0ZxQ2K%ZW7QP-v%W3gzim#*AeHy44zf)hjwz_w|O}NAy(cpO51ybPq(w~ zHk>&B@;hLs8QE7Ui6YlHpm!#TNC|^3V7RB~xpYd3zs^kiwbE+7LeeMPYe@d-rGZes z3(-_stli%Fv>qtk5@CR~`T!zQ^J(h1{?c<x&yYNxNc=Q?Y^kMh`7LbIU<YLM`;~TQ zvb<sn3e=3O($i(^pAtZXnF8=c7zw<!l;)p}!%I;QK`fw)|0_+X3PRH(p=?Y5;>gRH z2=ug<W<C0{ngX0y<g~~RITR)j7RbX@Ku{S6xGJkn>u09t>x$JchLiy@SVH;|muezL zXHPbF9}EUJ{S`AcJw{K(g0Mm*mLW%#LT>-UjZuJ0szf@r1Mo0qB_ZyWJ)su9Ts%+Z zW*M@h65}qyG`N?IL){N297;fTVq?`D)hcL`7hrTAJehhpAYdL=E$CG#qjAevP!oGM z?k&!PwK?&ud@|>FBUhq61>ol|3a8)qLLb|-*vN_g?kdZSIlfxiveVJd1Ous=8&@II zn73qbx8RAujG|LDcxZ@7sM(iPi{b~t;{GKiP^&s<o#$CPYF^8cITxpHfuVrFE)X8C z@rg_Iu{N$-w}TiT;O$KmQCwaxeqj;q#y_klpiGcuk`+zp-2r>S6V>;IEfx#vx7Pch zAAvV2RzX)~I-@=d!cWe%7tWJyDe9T1;NJz4+*c>|^AckjCn9%dJmpV=&eTn|CICs^ z$7tK37|IVJ1$*LN$97!6GVdkVav?V{g_OqUj|Q2Jc0qI5h)BuabOgylLlZqA@cRWP zlLIxx=8zOSDWIqKk(}B54h;V9Q>@kciUz-g++}<N89AQA!KL7s^g9@TGhC%%MsL{P ze9tg<x{>&BPOb)jyQ(CMhb48hK?M0%Eg7Y^oYI}x5gai4708|t|G%x#4=H|8WCq8B zK3*+!7^01L&?>D+Z+jz<JrBsJRCgdjn|&;>C^~iSlLzC0C%5c#Xu7(97+x;BmdIjH zllh2l!A#e6qC>u0Yy*R%%`#paRRCMYZP8qNwH$q6zY>ng?vml?$^Cvk_}r#Rt57-# z6x(+k?hD`#u>%Yexzg5;aYYlMf?s~w;U1Am6&eO6i9%kjZfw5i6WK*V-#!*YcpN>i zzZU(1l@0;1&dmje$mXpa)xSH%joG4ts9d|(Yit-0!LeE)0NsOYZGCw42tTl{`GH$) zy$JmNcLw>l(&aL2`J>{RGCt^SLtlEDh#2JIN$xTS3*D|BO9L!*E@v2!Ox~)jLldR@ zgak4{LSO9XrdQOhD;$5#AvCVwuR22o71Q7Dn76+%x&!MZKxZAw0XZ#HBo6(TIorJ9 zO%yHa-w!)VI1zOO$!dd2bXt&M;{)_LWho6!_D|dB{e$O%1L-nd-U=^+130HQmDUXS zxUL&?Ij_1M{>HX=*BMPy4!2=D++INK^rHjKW;EH#Sa4e>7z!^Mrz$1RU=7afj9J3< z^)tVY1*R=EfcvwtihQfRmVYGl&@HqGQJ!PNZuwsIbq+hlJyz1(MiyHW<AWyX`y)P3 zRckbf$%?9W<<z@1ywmfs-HI2+?qA*Q{e^R^F#~f)BvgAi{+auoIXB5;#jUZwp-gne zT+0!x3ju~EIhD8{k*=DRE4ykuS%tdz)dV@3Guv!@629{0^vX&F($ZWOiD$wcmS*j~ zh`x}`?x_v>BiNEfgN%pj_OsK8fDJ~oJoF6&Gzh^@%kQcIP}cv$p^Igi6TCvIN2%;N zT^;}2m-9r<wz|HG4hZ8=9S#tT4_Iap9T-N};h4v^ZihST{5_*c6#6%;Hw5dGKUyaT zl|cXi3W;;0?e0tE>j*dY`c2ZLpC0MVFK~}7XiyvB8vJ``QzX+o!Ir;Z7Mq$lkFPXS z`l`f;bZ%7w9TE5u`j1X{2K!$9aJYmW(b$bq8q|7C$+_2{te5w*v`tzq92D|~-(<>S zzuo`PnGqxHdx>fuPZ$T-JF>ktrx+5A3Kl5wM*l5ja1SIZr-m!u)^Zie`#HYUsp)`f z{S54m^Gjgab~$1^>4&f>hYAYOGYwL6Hb3X6!97m}di6ykXVjjhBAQd_n*EOdOat_> z^fd|_x<bEzlmb>^tIrcbSbO5E{66X{pT`t_C_^eE0z6xA5Vv#K+pMCX4i00jti3(2 zKC8{~T5$IOSa&b-lO9GvfwaFX51_}p?C%j~9S8nz$KLC}f&7v$sR9Bw$9@V@VzE1L z*ocl7$d$O>l?Vn(Q}uYcVY^V&l-j4VE#u_oxEdOd*@l**hzymjc%sqvb6wD_lj)g% zru@V~3!f1aHmMO*CpdT2V_;yftLrmOi`g9nqOfSI)Cslj_>2BU#uoIU*|vv@=`^E! zVSV`wdpoKlQbi!>BaXdpqY7uB9XG?^qXPEZF%9f=xv8xJ*8b|Tp=1ylg?M!@t!`Bu zVp$<XB~L8jYlM-kwL62`J8{6P)OJCuUl+?8A)YA`Kymjvn<JU!Ox1S6h12&+`@6nh z<|Mat>nKo{7$*li4)mvu!Q%3V770$8#YTsTmovz4v8W9DaI$l!wh@`vmog)P2b%Jh zWa&85cZM$prn%ntWf|~x^aW3odd?X~BN){LO~?|gvH6th|6OH}vfD|#=NlMFv^Thw zmfZ(9?8d+~W#)-Q;|o*IGEl+impY`*MmGD&uP~t`rW(s2sF?6v5kkA_5fT=WmqP!R zmL@p|J4BN#{t{A4PkXEnf%uX;E#Z(2HK-bSeJSw6G5kt{LBUO0xZCQ<d;rcjZNV!+ zX#y2YA_h^9DF(XI6X)>oTvTMUHVRIDr%=^+fFPftE)d2|Q;XB`0?nr`9>|XcqCWnF zR0bnI)K_F{%E$d0yuWCl8PB#y=mopC;*sl0S{O@X|Hzbr_?;s1-wO0%c!A7jCK5`; z=F+wJj++!L7p~tdreEvXt&*)v1;5g70Y|!2i$4S``<y*sC@4Fy32Y&;MgYXz$U-;V zQvc6m(lbAIJUc_>*6fOb{SA;F?7bZGI8pzeMKv98l2L9uXx`;;zzywVQN>sX>apee zk60@>dn7XrVn0+=P=BKcfX&oEI<nnaPzIz|lxA9(!>LJHvI|GRY?4$O0X}2M!I?~8 z7_1pVUJd2?#!5+_hXN}sjA~NKu{XPQ#i9agdF^fH?+qrkt+!_Z_eTfR?|}_z>K=AN zF!L4_+i>0G0D2pC)?hn5flt%Rn@hQGwi3v_y%GboF^CrFmy%`!JMiI4B5m_T`CLgp z5>*c0TBQz~6%TL;sQNC>``Rc8nYPZTR1|{3)(?q!SoWQydaz?-k7+z1vx8BU8HshE znwhxk-JfW1tnYZqSe$IZ5>K2XmkA?0!i@Ldn2AbDrFS#W@<Wz-R=LvMUGtTuwU}%` zs5*qE0$69>pcIo8qu=BbpbaGt?khk3l{shEh+Rv85w+4$X9ya~G!}&z?bvAo+y`mb zYl~PN)yrG}4-M*}1qF!bCr%l*OEvbkN|o{AVpbP~T2>LugyThBSPxZM;>AN3Wq;vk znoBTQJk(q^k_%aWWwN^v+42tcOjkbc066=<WB$diBr_(24f+VL8yXjRgJ^+Thy#47 zSG?wd?G672%bHs`kqhO`zAP_CK%cGDiPF2V_R=Z1cBB(-D{~k~O_#y%!3|{);rka6 zL<*&=*kgG?&U554+<{tsHdMIcC-Aou<c|2i_06_+Lc)VhBe(2mk2n)x9h=#N02HU@ zZ@ka!!53T?{ZjhAS^e|!hh4(r(^6+ZjeO$}xd7F&EY$xbL4Jj~>`lh1O9aH{l}|XR z!d#PDAT>oT5vs?!r*kzB6ggG?>XIl+Z-a?ZcV)byQPo%~%l$}#s0bpqC>Ze1QuxVm z-9gj<RAn;jw!(fW7VfvC1g=tWGlM6ssLH^+XIx)PTPw-SY8Ti)J3_0vZ9Wq)A*m(9 zFcHMjHg#UBpSUX>*L!7T0aI~X8#tj-uhZZgo-BYQ4HB)lGI}fXx*uqcd>dOI=|M@% z4jZXxakk!8&|E#f7M&RDiM>?;{Necdr%zHDw+~cI^sdJES}hwuLN}y+_y{*;XL!Uh z&DzLZyre$onzDv{S>OlyUK`->lV<n(eGp#E=>N8mPM5<u*LW;Cl=#KCXEiun)pM^o z#~(rFk9^g8+?dhhpG^9(A(Si>DG<Q=0fA`K{a$xV@X~rqdXANR^zOkq9Mc&H!8g3Z zz}cFztwMzgWI@L3My4splLo;>;}N+@vz1-PX<jD9VJynPINI_B2Ezw;k%8&g%;{u) ztD@#+6o8YPthVwR6aqR{3?T_GjYOnOCht$Cz`oJRtyKT84_+$Ix-)p>PTJRCx6+VJ zDVZPkK)S|_><M0dT1o{^7bh%{<~_+5!B+W~SC=`T)6#iB?rrVbvo1WR3xrSj<3#Ee zrsWDc;}xtz^qMtqDo^KC)0<>Ufl$7M!5bYh{F4!}mjgB1;NgOOtW<^$nfc|dbSQ?a zfD|)3M`l%8D2(9tQ<Rv{IT}V)E0BGa<+s)pU4Ru~vXP_V;A?qO(%IVGUJS}iU<jK- zL@Us7tPe`p6P1F*((4CXXBVi~CJEE4Rf$v4Qd)j<QzBNHu+=0WsHpa-{RsZYy>tU* zIjr=d1~rupi8t4nL=U5v3@>mNq|IpnBaT^_CLX2){Dr*1?W#Fhb_X_=AMb8@9p5*_ zd;a3@o>rUBI=WcLFwNsDLkaQ_miyCEpf4p#@nE07vFu*F`U^`DvJ+Sz0RAm#03Ue0 z4F)-#Q}h0XXqg9|uuP;Kye!<run}Yz3gZrXp6{MjuLSwoAGl4M4J3FHhDVhy4naPh zZQUDDNSWSmnLYl%a?BiM1sLfDdIA88nwl%}_7d}F;lw*~;lG210B6X5xwx`G5Cfzx z$w|b5WT3YI<4}a7Y`8!m{v3XX2pnX5q7dsx51$nDM?2i$gRN~tzb#cbD|Ro7>ag|% z;GHB;DUb?z0e|tKmpxV`A3@q`j1@rM+>^KRNr<WQ60>9go<-*%yYx;oS@TO8<GQ{V z@|`hiB(!;*E*u|aW-#^(s1<e<nicm0draM1{a>bZVM<!cTCh-pUFODVq8vDnoO2a& zZ0td%*hEx~UUWe=8xGZUG$|$y1Kj|ANk?L<K!Rj&!4$Z-&2uSfo3TilgH;S#6*)x) zTDuagz_1yuj{wsq8q7@{EDBjJAZ4OZcy<l6PIZ4f25-(uxz*&bXCA`Hjm0m3hBAL# z5!(X1&H^aUVAi={nmAtlV1DS50TDLnv4I}hnf{6l3~mgh;eqPu_n?(kJzxCju;tyT z{s2w|1_5Bx&85P2wVu^B*P%T$z+sauIe~&tXY)kbXiQ}%SKFa<iDGK16UhY7FA<>i z7qXtc<A;1bg^G*+1;MU$rTVNvIt>Ql;^&^qY0e%RdE)BsyaCbipA_JuS{MG4z;oy_ zu-@oMN0@CW<kB*QTDE#8+Kwf)vuWAPEoPlw$6Q-+m94BElA!=`XcAc6TvmYwE~NbM zKELhGQXG$JgX(4E+*C5%0bSeUc0Me6tW+4|TVj38ceMEclNo_j#E$arl|+X35-|X7 z&!1KceZ09T18m3Ot(!(lZ(zVzpe?IJG}QKu<zoO)^b6f0BG&1UnCKO=%vlmlY5ydN zL`4%9__<>fcRA7$(jb(0QL~?TEDp+cy0Gelwho{=JXzQdBW;+%j?j#0YWb%EQQQWI z4>yHm$uPlvDsvytpi;D};`hYAy^AR+Bm)J1*Bp2DSZMKqlN-%7N_1T&wI?I;O3(k^ z$fcYRB1r+!E+omlVM7JH8>HT~-Ug#<Q=$6kCsgF!N&*^o9u0+ehF^UN@VsiI0lGh~ zr|g8oCY}k|#GDRRrFbfw@d?88!CXAg^3~|~M+B4deOMYf?y1*Qp#A}1)Z#+gGU?}o z@2>ZHmo|UK?M<nv`3tCQK!kVH9||B!;ntF#8LEA;<w|edej<soUcitgFRcaKtCvlA zjhWL?#p`TRI|!bvPT4QgsfL84YRMf}i>=`)q4;_+b$%aaRWj>6S1b1a{QOQssRW0O z7j_W^C;<amLhi}KW~F=i``aoYd{+OQGGyO_FOP212|*>-3=QH>1*iWyMv}>z(1N&; z?i1E`vOgU3eae74xHtP?v8sbdXtdXk;3Nrr?T<Owq}01?&H1G}voV{HbW2p7$O37} zi<@}|e>d6fC`vK6Bl8}CWNV?&0{Ufy9RpF{Rl{ijEYeJcv4>;8dVP@kvKOOWc<p=; zmTRI$FkE^o0CtUxqtw-YuRdp!87^>gsp=#*FRUXb=^%)@F3JfJ$_Aar*cE4N7)GAL z9Vklof*q<#?{e-^j}r^q&XX*Ox9<-rPhVJ)N4;Y}&^@xZ$_$Ip-kafCZ<JDUkQV0* z5``v%;7<OR(4zApPFYlX<(4UX?GWlv#>pPuSpC?riWR@`XSMB|tn|Zo>@l>8iv$%4 z2D(jH>71f*Xi%9qOgO$tm{g&Z8Vsax{O6fzJ_ej)>ed8oEUU*|AJ!o|&{(;E6uo~Z zbbqw*CSJ)<>#9Pja~Z!j@N#P+I?ljSqYu%i5`?w%oqD&Je@MsACGbj6T9KLeoAiF` z{DwM!T%?$3=2+=hR|F)!r~dpc><z*{WGI?23LJxt`tSTJbY31|*ou|9v9k-HBF+9A z%`ToiP!Ee*=1yMrm}17=76b|~nk5i94OG%8xFeEIBMhjfTl>-YnlMc`V(lP`-*7u+ z+^FSG7f)0NNS?~mg$TO{qO7!fl|Ph3n_u;EcFU6Wiina9;+aF(+^N1Iml4Ndrq&t_ z1@|&cPNa4O`xwi(@Y?wPS;>21@NpsbKKss0_u1MtfUZ2<nf&{kvmfmQc%FLb2%Z70 z{TaD-#Tky1tJ=>3-0%(U=HB3y!Wy|r$4>{-lwQo!o+Wqbj$c58t(u&vN+KBm<3j9$ z(+15;TXZ^3=U8MIo!@eEXBBVvo_|*F!>qyxfqXjDoG|96OJ(uw1E|g5Ya_p-cL6%G zQEo^KraVvib`W#+JB@fno@v$hHtgn*!jVI_fNP*#Zn{8*LPfSNHp?(|A`kglf;X*y z%Nd1(_H{cPc!y`t&hRpoW|cv<UEsUf+EKEP6R~$O9bl&(T4_JS9~QUzw?ofotTB;k z?;l_0EFfIhm@7$tthFZlrhXd-LJyX)@`;@j`wSR&46Uca_6cR4k+V^R<Xfgpm7xZW zF2T>jl~xIviZ2H(#djE5ynNqdgqEtUP|?*Gz6upB>IK07BAb}nXu%_>fAnhw4(^@v zvIpAgA6x}O@;`|vA&?h0Y&F-T%jqJ)x)-P-fETYvpNm_XQl0TwYPtTDX3z743E(*` zpuTce-7^(c=pb(lNhw1N?0Ap!F?wBm&<d23-dtD;HeK(C`%U?{xpu~zF*KL7q(C2; zOHU=|y>?{F_m@0vpNeTt;eXHwPY?;@mn4b|5Y;B}J|haLijC9I;ODPoU{Cy)B%mew zAw=Il67E1u+aq%ktWD2baT9ybmcBy`^%yBJ_!uYGMl*SWLNtLY^ktmbbvJ;tPAB)` z;<$Av;K3x(odJui5*FuUB8*3OirE$tOJo-JYmN+-f9it7JMjGAOc$0^;JbK16Q(U& zbD7ZPIsJ)JO$8dBkO^msWX>Mzch|%B4{ZRWXGv)BzfeRDD%|)2f$gK2RV?lrLN<i! zgpYTY;R6C2@8EUVvm|u=6rpFSpdC4nyEy`xhzj7nmv2GCEgjxq04|JjJhvH#wuF5l z;0jbw39sdLEE$3)olGNtZ8pK(rbrdOC9iNbexxbFiE<YS1fVGY#fsrr@(Y35@er#C z`8qe0&j9|3X{5%aQ|@X?2bjQoSRvjh6?H6kY7?8q2fg`;d(q6Z-Jfj~Q4<XK^1me? zA}Kk|)I1+MPz4C?hkE~eGxupjEv+e|H@LZA1DU?7scK3A?-3Cf#S$Zo8L<zi*PGvf zqv!U`>rmiDmN2G1>JWo4m+%g4WB~^p>0s>%*fz@`v=4*VhX-^DdTWB*JLjU^I3T<r zy{JZ}GHb7^YBYce!y9fMHVw{%{MLSgkw-PHI335AHw70BgULsmLIda@mEq<Wm)|pg z;yEp>wAM&u!GBmqr4$lxbkqCi1?Ca21OED<V*-wRA}bQhN)1uy5o`S)tCAv;)Cc=K z>TgyA%+5WNr2xoiVs~k;$qt{h5;Qv~Tnwz1#4I1tA_Hl-PO)D=McB})#GBTcxRV8< zu64TjzCs~}E;213+(wlt1l0lfqz91tgk2tAU{1YwV|Nq3t-n*%t#*UaQaD!kg{|n| z1>$l5Ia9{F1@%H>ps+m^L<^(VJo02Jm-6p^Bk}GrEh+){;P+1bzg~u;SW1R`he6_s zaF<SxH`XOl8nV7G1{W{+j5X3mOYrMpt^$;+u!1*3y}McyxaKoDeyLU(Q4OPQ^#Pv= zN)MQmm$ajh*##UvR)L+qg@G#sz=)_1mKZb}J04`RC2HC<Xo<w3ntvCYRmjIXA8ZDU z?bn9`V-g&j5a4x%x$f}|$I%eslzEo{aqg;tc|dCeAhL5>a4Q2UEU*rvap~m3$tkN) zwh=WWjUzTf<e-uaFkH>H2#{NgpbV`+_|Rgd0&p^u7~Rc$$8|FXM2?l~)HA3*2LZN6 z;>BOKZBlr1ntQ%vdw@bj2L90~+j>2Ue}U7JS?K1R=(t_4<CnhYR1IdZzYE9OykwW? zv5-rRID>7>a=VBntkJS#J1vahNiS-ka!I&2C)5HxAB$N>s3*;6wi4q6@#=eKDY%W2 zj(U}j@cr4qt}zrwA_M{3R42z8KL&F$`3VCP9gb|XeO=~as0M-!v2~+2cog)LennB4 zim<Zu4G{jNQdLuA#LKr&+yof5j<t#VLij&<x7GlugBTJgtGEVDIfadM`vYQ(n$l1a zm2uORYU@_VnToMAn7_S)h;YL}DmF}_zg+aFSPT`&&j`N>=0@Px#<RYXTO{Q_Z#^4> zN+M+5&wQN^YQ6S0Lwn57`bkm4aE>}owG4a==vGYo_1^&xLYX^>x3|6Qh#1yNv+zT` zJzyMAI94h{;Qs$e$@p0^oZg`Pbqsbb4$&@s2m!GmS(NFEhQm1&MHE$x>FmT-hr-O7 zI+#9=DNi0Da<tdNb_dGlhXqsmGKd*#TM`;;k0#8?hv97}+`{SkO?IHO?aRCjMy5ec zBm!w`4kzb4ClPQ;IspIzgjT8_+HP<Si*o-~3TVAHeA-9mZ?cJlhi7*@>I&<qXut|~ zpOjcr;HvlFnhxGqEvxsM>%u-~X7SbHK_YQ3gD$-Y&f=GTj0#ggPv{|Zv6K`&{W&0p zs0OR1zY2L@I63S=+WDFzXj^pUtM~WeXEJVW9i$%x#WeKi$t78T3onL8w8S&J@>hKb zNfD&nuKguH-ln>V6RJufw8_ITuj~@>R<`$4XBT06j*Ox^>y2E;wM%vAD@+zRIuWj2 zAzrp0v;sq)_4zFh2Dus)tikZ6gm>QjHr6+kq-I=h(D9w@O+CW9Xny5X;Ru?+=k)^M z-H_w)g;>YD$iRc)$rWHy#QO_{-nek{4(^m34t!sRVJ|HL5DTUWJr>W;eKXfM6LV5k zw*+~r0^+8({5RT3zKLoD-xVTv3&DJ76W7W&R#u7WZFCKJ773sgtw0!V&CGKH(ue=f zv}w`A95xLvqrO$@4PlrSVr7RN5DLp#TRMr7P(eNd#}s^Ge4oo1J6~;D%e)Dz@CFcq zlB1%tHeY-7Gtx49SdVn#Fv)iWr|ESaU_yyY5Cw*o0%E;afmnfincK_w!vIa8=P{e* zL~T6Y^=1-nZLT%A(I{Y5rIx9*$Z|^d(+A8cYK7Q2y=xcx=5;^lPeS*&CdO4o(`g&9 zdg$LkqHMsJFn$gSqVnY#qrU1e4-dsFaPix1-g_<L#&tmN?x-CW`m;lg(@3uUfD<d_ z@iBJ$Ug~eqRlV^FUOH@t0SAG#w9`mM8Dn)BaGG4mdKrgj&1A-301JK~#Go&gg8O!3 zeuUP;Dno17!5aZt@&jwlT8t$Wc*+9B+kq>o_RjVkWhRI=LszUut$j>0LhpJe;eb)W zUk_UfF{K%p#tk7#7%nX8u4MF_-+4u-`M>M!dPmfFxCBn4H+y&g${QM#7AIqV!{Bc? znd5deRszmD+=L>72wMzmV4C@vjE-x8MAx&-sn&CzbhZ|t*+%6C1ApHOdlg50zy-{K zauHXy!HH*j#Lr;!2G_nu@2OgOVPpo$Sz{<_NB;}N?Fn3c{RSjf?;|nLG6|Flnhi8Q z9IOZ6+LV>2Gqty(S-qEs;@<5OgVm!wx0>eb)3C5}C}L|16tA(cu@HCBxe}IU47co0 z=Z#?;<mX3Nd9go4Xqwn4k&gn22D+gHIzdX-3Nl3)c0Al`3*xeeMG#ptrk5vRBUSUt z9w<pdQ4xDbD2(N>wO29^aEI8%kHqYPD2><}d`JQBq5Px^1rr3}sqKZ%k>D$!;n(zU zaYwk&0IP;<!W%I=v;yRH68RFm5bC|4apo+m_S7}Sn+J9HfPnKlFP2Hv=f<EsuNQBg z@nU75^`FNs2+5w^5e#;$P@5O|c$;>ylKyUe?gKT$!>N&B^v3skN<v~0pLC8##wiQh z3C={yq5CQTVZkf^lLR%i1xrD^<T@(XZ4R3*&QV1%j|r(aBjdX1RqPljPZZNFQ#MC6 zLRFmc6$%3oLM&1zm><^X;^|?LC<aNOAND+t^OP7-Xo=#pd{B{qy8y5$>mxGOAQRKI z?rPFQ@Hw)M@Ou5!IM3R4>lPk~FLGhZS?=3k8ZZRhRsz+lXLKbM2;)*+P1Lq0#5d^k zWF`z_B93G}JGsQYK>{@nVb91Io}VbK*!x38VtQ}5FhZ3yK$d<e;+5D&EEZ9OQWSpj zN)pbhbE4$(T>?)tGc6?3q)f}sIFEfM+rr;`I{O}JObDwqe1I161^CL(r9`{01Vw-# z1JKzE8WdH~sdUZSQ84P-UhshImBEj%=v&Ks;P<vrn)bct5cWrdlX~Egnot?D4p(3Y za1bpt7LSi>ZK7hfbq~ui$=)H-VTDo-2cvzPUewFfRa&{us(`YQT|R5OsYOV6iw2aH zT0ldq=R1ndeo!T!n+S@7T(Eu&^NEd>?XVk1+{bR*yy<@Ghmk*47vw=SAOSV!EiR@8 zQYV!9tr*@O)_C5?!KdB7*1&gcz}tV}mE5mm)FM5P4&#Uw4Wge*2?SFG8&UEyopEDE z3a(gg?5(*CI(SpFD>*SL4p-#K)I$2rxZ^W4bY7?~Uinp*x(1j-!-0T5{_ZN6^S`={ zWW>@}3ZVTg2`z<YitYghjwW+Yy&rSCeVm}|_nt4jItfd|@-^X@x->GL@3S7LJfaXC zbj@wQeaA+knY_IUx+%PJV;@sC0Ayxki#tUK5)LDR9L{}%p48baPc58K{)x)5a;rnM z>w@reAfm5C1zRM85G9h@OI5~L_j8BQX$4pG5?y-?Q}X@Vnd<?E_Qp&7o$6cJMHb0# zwN>WQ|M5Ur2m7pEF)r(c#!;6f1qthIKM8TGWO`iOZ0j5=^-L~()%RYE_^6&$Q%MfV zf;i7sZfZ)N@T3VU&Z6M{W&l5BPx0U1Ax%iO$7T49f=QXt8Fo9tC;ao!CNwe5^UknS zyyJwr6jTB9F84C^T^7f4*R$-xdZ_v=7MC0ZgaMLNGfN(E(K$3dPhW+T!uM^4{jkKG zPwr-u{(ZQ&Xa+rKBFJQ{+?EPFXNpQWryi>Z<}s-n+LY^lk>tRFEPctF0NOk<1?_fM z0odRe_ZenaGL1*YoulJ@FGwHv!H7}t0fH<D%70NJaH*DptGAWv9Q>VL`pu2B$Vs9x zkOR<$*gvvp>&iRp+kNOmr-rZOtiP~w7X)c#jBbw0wXqGAzN{m2ooAZH9U+yc_6l+q z2Kv%}RznKoc0Ti5YH(X|XNjsNW;ndseJiauqupQ*6^HS9SfVG4!&@VoHx-Y87T_+0 zXE)!>+R&%*0QeKbZ<;kUo_?k;tg?bkl6So{=2#v0J?itpCg&3IUJEbS>x$^&-%^3( zdcR{d${xdb1`$Kfd>nvE*~AINEvq!gjh%S}sC7)TfqIk2>J{RV1*iVM*v%WK%#Cyb zy6-<C63sqD#eQrEvH0?_I*azk{;I%YzSqZobH%@;=mCl@WGq<Tega;)yzO*yM1_E` g+;ght)L|-d70?SDFO6{n3KP7a5Wqj)F^xR@qeTkEDF6Tf diff --git a/substrate/primitives/crypto/ec-utils/src/utils.rs b/substrate/primitives/crypto/ec-utils/src/utils.rs index 5560d59211605..063b8fac7ad3f 100644 --- a/substrate/primitives/crypto/ec-utils/src/utils.rs +++ b/substrate/primitives/crypto/ec-utils/src/utils.rs @@ -15,8 +15,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! The generic executions of the operations on arkworks elliptic curves -//! which get instantiatied by the corresponding curves. +//! Generic executions of the operations for *Arkworks* elliptic curves. + use ark_ec::{ pairing::{MillerLoopOutput, Pairing, PairingOutput}, short_weierstrass, @@ -25,17 +25,18 @@ use ark_ec::{ twisted_edwards::TECurveConfig, CurveConfig, VariableBaseMSM, }; -use ark_scale::hazmat::ArkScaleProjective; -use ark_std::vec::Vec; -use codec::{Decode, Encode}; +use ark_scale::{ + hazmat::ArkScaleProjective, + scale::{Decode, Encode}, +}; +use sp_std::vec::Vec; -const HOST_CALL: ark_scale::Usage = ark_scale::HOST_CALL; -type ArkScale<T> = ark_scale::ArkScale<T, HOST_CALL>; +// Scale codec type which is expected to be used by the host functions. +// +// Encoding is set to `HOST_CALL` which is a shortcut for "not-validated" and "not-compressed". +type ArkScale<T> = ark_scale::ArkScale<T, { ark_scale::HOST_CALL }>; -pub(crate) fn multi_miller_loop_generic<Curve: Pairing>( - g1: Vec<u8>, - g2: Vec<u8>, -) -> Result<Vec<u8>, ()> { +pub fn multi_miller_loop<Curve: Pairing>(g1: Vec<u8>, g2: Vec<u8>) -> Result<Vec<u8>, ()> { let g1 = <ArkScale<Vec<<Curve as Pairing>::G1Affine>> as Decode>::decode(&mut g1.as_slice()) .map_err(|_| ())?; let g2 = <ArkScale<Vec<<Curve as Pairing>::G2Affine>> as Decode>::decode(&mut g2.as_slice()) @@ -47,7 +48,7 @@ pub(crate) fn multi_miller_loop_generic<Curve: Pairing>( Ok(result.encode()) } -pub(crate) fn final_exponentiation_generic<Curve: Pairing>(target: Vec<u8>) -> Result<Vec<u8>, ()> { +pub fn final_exponentiation<Curve: Pairing>(target: Vec<u8>) -> Result<Vec<u8>, ()> { let target = <ArkScale<<Curve as Pairing>::TargetField> as Decode>::decode(&mut target.as_slice()) .map_err(|_| ())?; @@ -58,10 +59,7 @@ pub(crate) fn final_exponentiation_generic<Curve: Pairing>(target: Vec<u8>) -> R Ok(result.encode()) } -pub(crate) fn msm_sw_generic<Curve: SWCurveConfig>( - bases: Vec<u8>, - scalars: Vec<u8>, -) -> Result<Vec<u8>, ()> { +pub fn msm_sw<Curve: SWCurveConfig>(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { let bases = <ArkScale<Vec<short_weierstrass::Affine<Curve>>> as Decode>::decode(&mut bases.as_slice()) .map_err(|_| ())?; @@ -78,10 +76,7 @@ pub(crate) fn msm_sw_generic<Curve: SWCurveConfig>( Ok(result.encode()) } -pub(crate) fn msm_te_generic<Curve: TECurveConfig>( - bases: Vec<u8>, - scalars: Vec<u8>, -) -> Result<Vec<u8>, ()> { +pub fn msm_te<Curve: TECurveConfig>(bases: Vec<u8>, scalars: Vec<u8>) -> Result<Vec<u8>, ()> { let bases = <ArkScale<Vec<twisted_edwards::Affine<Curve>>> as Decode>::decode(&mut bases.as_slice()) .map_err(|_| ())?; @@ -97,7 +92,7 @@ pub(crate) fn msm_te_generic<Curve: TECurveConfig>( Ok(result.encode()) } -pub(crate) fn mul_projective_generic<Group: SWCurveConfig>( +pub fn mul_projective_sw<Group: SWCurveConfig>( base: Vec<u8>, scalar: Vec<u8>, ) -> Result<Vec<u8>, ()> { @@ -113,7 +108,7 @@ pub(crate) fn mul_projective_generic<Group: SWCurveConfig>( Ok(result.encode()) } -pub(crate) fn mul_projective_te_generic<Group: TECurveConfig>( +pub fn mul_projective_te<Group: TECurveConfig>( base: Vec<u8>, scalar: Vec<u8>, ) -> Result<Vec<u8>, ()> { From 86fde367c0b10303b71a5f52857e254a2ee1e953 Mon Sep 17 00:00:00 2001 From: Alejandro Martinez Andres <11448715+al3mart@users.noreply.github.com> Date: Mon, 16 Oct 2023 10:47:20 +0200 Subject: [PATCH 53/54] Adding migrations to clean Rococo Gov 1 storage & reserved funds (#1849) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Following [polkadot#7314](https://github.com/paritytech/polkadot/pull/7314) and after merging https://github.com/paritytech/polkadot-sdk/pull/1177 this PR solves https://github.com/paritytech/polkadot-sdk/issues/1618 The following is a summary of the outcome of the migration. | Module | Total Accounts | Total stake to unlock | Total deposit to unreserve | | ------- | --------------- | --------------------- | -------------------------- | | Elections Phragmen | 27 | 1,132.821063320441 ROC | 1.465386531600 ROC | | Democracy | 69 | 2733.923509345613 ROC | 0.166666665000 ROC | | Tips | 4 | N/A | 0.015099999849 ROC | The migrations will also remove the following amount of keys 103 Democracy keys 🧹 5 Council keys 🧹 1 TechnicalCommittee keys 🧹 25 PhragmenElection keys 🧹 1 TechnicalMembership keys 🧹 9 Tips keys 🧹 --- polkadot/runtime/rococo/src/lib.rs | 61 ++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 9933f64429745..3c08b9b2f94ef 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -1414,6 +1414,52 @@ pub type Migrations = migrations::Unreleased; pub mod migrations { use super::*; + use frame_support::traits::LockIdentifier; + use frame_system::pallet_prelude::BlockNumberFor; + + parameter_types! { + pub const DemocracyPalletName: &'static str = "Democracy"; + pub const CouncilPalletName: &'static str = "Council"; + pub const TechnicalCommitteePalletName: &'static str = "TechnicalCommittee"; + pub const PhragmenElectionPalletName: &'static str = "PhragmenElection"; + pub const TechnicalMembershipPalletName: &'static str = "TechnicalMembership"; + pub const TipsPalletName: &'static str = "Tips"; + pub const PhragmenElectionPalletId: LockIdentifier = *b"phrelect"; + } + + // Special Config for Gov V1 pallets, allowing us to run migrations for them without + // implementing their configs on [`Runtime`]. + pub struct UnlockConfig; + impl pallet_democracy::migrations::unlock_and_unreserve_all_funds::UnlockConfig for UnlockConfig { + type Currency = Balances; + type MaxVotes = ConstU32<100>; + type MaxDeposits = ConstU32<100>; + type AccountId = AccountId; + type BlockNumber = BlockNumberFor<Runtime>; + type DbWeight = <Runtime as frame_system::Config>::DbWeight; + type PalletName = DemocracyPalletName; + } + impl pallet_elections_phragmen::migrations::unlock_and_unreserve_all_funds::UnlockConfig + for UnlockConfig + { + type Currency = Balances; + type MaxVotesPerVoter = ConstU32<16>; + type PalletId = PhragmenElectionPalletId; + type AccountId = AccountId; + type DbWeight = <Runtime as frame_system::Config>::DbWeight; + type PalletName = PhragmenElectionPalletName; + } + impl pallet_tips::migrations::unreserve_deposits::UnlockConfig<()> for UnlockConfig { + type Currency = Balances; + type Hash = Hash; + type DataDepositPerByte = DataDepositPerByte; + type TipReportDepositBase = TipReportDepositBase; + type AccountId = AccountId; + type BlockNumber = BlockNumberFor<Runtime>; + type DbWeight = <Runtime as frame_system::Config>::DbWeight; + type PalletName = TipsPalletName; + } + /// Unreleased migrations. Add new ones here: pub type Unreleased = ( pallet_society::migrations::VersionCheckedMigrateToV2<Runtime, (), ()>, @@ -1426,6 +1472,21 @@ pub mod migrations { paras_registrar::migration::VersionCheckedMigrateToV1<Runtime, ()>, pallet_referenda::migration::v1::MigrateV0ToV1<Runtime, ()>, pallet_referenda::migration::v1::MigrateV0ToV1<Runtime, pallet_referenda::Instance2>, + + // Unlock & unreserve Gov1 funds + + pallet_elections_phragmen::migrations::unlock_and_unreserve_all_funds::UnlockAndUnreserveAllFunds<UnlockConfig>, + pallet_democracy::migrations::unlock_and_unreserve_all_funds::UnlockAndUnreserveAllFunds<UnlockConfig>, + pallet_tips::migrations::unreserve_deposits::UnreserveDeposits<UnlockConfig, ()>, + + // Delete all Gov v1 pallet storage key/values. + + frame_support::migrations::RemovePallet<DemocracyPalletName, <Runtime as frame_system::Config>::DbWeight>, + frame_support::migrations::RemovePallet<CouncilPalletName, <Runtime as frame_system::Config>::DbWeight>, + frame_support::migrations::RemovePallet<TechnicalCommitteePalletName, <Runtime as frame_system::Config>::DbWeight>, + frame_support::migrations::RemovePallet<PhragmenElectionPalletName, <Runtime as frame_system::Config>::DbWeight>, + frame_support::migrations::RemovePallet<TechnicalMembershipPalletName, <Runtime as frame_system::Config>::DbWeight>, + frame_support::migrations::RemovePallet<TipsPalletName, <Runtime as frame_system::Config>::DbWeight>, ); } From 8f1f2f35ba704981ae90b67f739e0f9485150b03 Mon Sep 17 00:00:00 2001 From: Ignacio Palacios <ignacio.palacios.santos@gmail.com> Date: Mon, 16 Oct 2023 11:09:46 +0200 Subject: [PATCH 54/54] Publish `xcm-emulator` crate (#1881) Remove `publish = false` to publish the crate --- cumulus/xcm/xcm-emulator/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/cumulus/xcm/xcm-emulator/Cargo.toml b/cumulus/xcm/xcm-emulator/Cargo.toml index 72007ba98f7fa..c77d350bdfef6 100644 --- a/cumulus/xcm/xcm-emulator/Cargo.toml +++ b/cumulus/xcm/xcm-emulator/Cargo.toml @@ -4,7 +4,6 @@ description = "Test kit to emulate XCM program execution." version = "0.1.0" authors.workspace = true edition.workspace = true -publish = false [dependencies] codec = { package = "parity-scale-codec", version = "3.0.0" }