From 8e0743a80e4760099e93674f521bfca5c02c014b Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Wed, 18 Sep 2024 18:24:35 +0200 Subject: [PATCH 01/17] WIP: one more bug --- metadata/src/lib.rs | 8 + metadata/src/utils/retain.rs | 478 ++++++++++++++++++++++++++++++++++- 2 files changed, 481 insertions(+), 5 deletions(-) diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index fb86ddcbef..79c771c197 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -340,6 +340,14 @@ impl StorageMetadata { self.entries.values() } + /// filter over the storage entries. + pub fn retain_entries(&mut self, f: F) + where + F: FnMut(&StorageEntryMetadata) -> bool, + { + self.entries.retain(f) + } + /// Return a specific storage entry given its name. pub fn entry_by_name(&self, name: &str) -> Option<&StorageEntryMetadata> { self.entries.get_by_key(name) diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index a160609597..7f01f9a0b2 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -4,13 +4,263 @@ //! Utility functions to generate a subset of the metadata. +use crate::utils::ordered_map::OrderedMap; use crate::{ ExtrinsicMetadata, Metadata, OuterEnumsMetadata, PalletMetadataInner, RuntimeApiMetadataInner, StorageEntryType, }; -use alloc::collections::BTreeMap; +use alloc::collections::{BTreeMap, VecDeque}; +use alloc::vec::Vec; use hashbrown::HashSet; -use scale_info::TypeDef; +use scale_info::{ + form::PortableForm, PortableType, TypeDef, TypeDefArray, TypeDefBitSequence, TypeDefCompact, + TypeDefComposite, TypeDefSequence, TypeDefTuple, TypeDefVariant, +}; + +struct TypeSet { + seen_ids: HashSet, +} + +impl TypeSet { + fn collect_types(&mut self, metadata: &Metadata, t: &PortableType) { + let mut work_set = VecDeque::from([t]); + while let Some(typ) = work_set.pop_front() { + match &typ.ty.type_def { + TypeDef::Composite(TypeDefComposite { fields }) => { + for field in fields { + if self.seen_ids.insert(field.ty.id) { + let ty = resolve_typ(metadata, field.ty.id); + work_set.push_back(ty); + } + } + } + TypeDef::Variant(TypeDefVariant { variants }) => { + for variant in variants { + for field in &variant.fields { + if self.seen_ids.insert(field.ty.id) { + let ty = resolve_typ(metadata, field.ty.id); + work_set.push_back(ty); + } + } + } + } + TypeDef::Array(TypeDefArray { len: _, type_param }) + | TypeDef::Sequence(TypeDefSequence { type_param }) + | TypeDef::Compact(TypeDefCompact { type_param }) => { + if self.seen_ids.insert(type_param.id) { + let ty = resolve_typ(metadata, type_param.id); + work_set.push_back(ty); + } + } + TypeDef::Tuple(TypeDefTuple { fields }) => { + for field in fields { + if self.seen_ids.insert(field.id) { + let ty = resolve_typ(metadata, field.id); + work_set.push_back(ty); + } + } + } + TypeDef::Primitive(_) => (), + TypeDef::BitSequence(TypeDefBitSequence { + bit_store_type, + bit_order_type, + }) => { + for typ in [bit_order_type, bit_store_type] { + if self.seen_ids.insert(typ.id) { + let ty = resolve_typ(metadata, typ.id); + work_set.push_back(ty); + } + } + } + } + } + } +} + +fn update_types(metadata: &Metadata, map_ids: BTreeMap, t: &PortableType) { + let mut work_set = VecDeque::from([t]); + while let Some(typ) = work_set.pop_front() { + match &typ.ty.type_def { + TypeDef::Composite(TypeDefComposite { fields }) => { + for field in fields { + let ty = resolve_typ(metadata, field.ty.id); + work_set.push_back(ty); + } + } + TypeDef::Variant(TypeDefVariant { variants }) => { + for variant in variants { + for field in &variant.fields { + let ty = resolve_typ(metadata, field.ty.id); + work_set.push_back(ty); + } + } + } + TypeDef::Array(TypeDefArray { len: _, type_param }) + | TypeDef::Sequence(TypeDefSequence { type_param }) + | TypeDef::Compact(TypeDefCompact { type_param }) => { + let ty = resolve_typ(metadata, type_param.id); + work_set.push_back(ty); + } + TypeDef::Tuple(TypeDefTuple { fields }) => { + for field in fields { + let ty = resolve_typ(metadata, field.id); + work_set.push_back(ty); + } + } + TypeDef::Primitive(_) => (), + TypeDef::BitSequence(TypeDefBitSequence { + bit_store_type, + bit_order_type, + }) => { + for typ in [bit_order_type, bit_store_type] { + let ty = resolve_typ(metadata, typ.id); + work_set.push_back(ty); + } + } + } + } +} + +fn resolve_typ(metadata: &Metadata, typ: u32) -> &PortableType { + metadata + .types + .types + .get(typ as usize) + .expect("Metadata should contain enum type in registry") +} +/// Collect all type IDs needed to represent the provided pallet. +fn collect_pallet_types2( + pallet: &PalletMetadataInner, + seen_set: &mut TypeSet, + metadata: &Metadata, +) { + let mut type_ids = Vec::new(); + if let Some(storage) = &pallet.storage { + for entry in storage.entries() { + match entry.entry_type { + StorageEntryType::Plain(ty) => { + type_ids.push(ty); + } + StorageEntryType::Map { + key_ty, value_ty, .. + } => { + type_ids.push(key_ty); + type_ids.push(value_ty); + } + } + } + } + + if let Some(ty) = pallet.call_ty { + type_ids.push(ty); + } + + if let Some(ty) = pallet.event_ty { + type_ids.push(ty); + } + + for constant in pallet.constants.values() { + type_ids.push(constant.ty); + } + + if let Some(ty) = pallet.error_ty { + type_ids.push(ty); + } + for id in type_ids { + if seen_set.seen_ids.insert(id) { + let typ = resolve_typ(metadata, id); + seen_set.collect_types(metadata, typ) + } + } +} +fn update_filtered_pallet(pallet: &mut PalletMetadataInner, seen_set: &mut TypeSet) { + pallet.storage.as_mut().and_then(|storage| { + storage.retain_entries(|entry| match entry.entry_type { + StorageEntryType::Plain(ty) => seen_set.seen_ids.contains(&ty), + StorageEntryType::Map { + key_ty, value_ty, .. + } => seen_set.seen_ids.contains(&key_ty) && seen_set.seen_ids.contains(&value_ty), + }); + if storage.entries().len() == 0 { + None + } else { + Some(storage) + } + }); + pallet.storage = None; + pallet.call_ty.and_then(|ty| { + if seen_set.seen_ids.contains(&ty) { + Some(ty) + } else { + None + } + }); + + pallet.event_ty.and_then(|ty| { + if seen_set.seen_ids.contains(&ty) { + Some(ty) + } else { + None + } + }); + + pallet.error_ty.and_then(|ty| { + if seen_set.seen_ids.contains(&ty) { + Some(ty) + } else { + None + } + }); + + pallet + .constants + .retain(|value| seen_set.seen_ids.contains(&value.ty)); + + pallet.constants = OrderedMap::new(); +} + +/// Collect all type IDs needed to represent the runtime APIs. +fn collect_runtime_api_types2( + api: &RuntimeApiMetadataInner, + seen_set: &mut TypeSet, + metadata: &Metadata, +) { + for method in api.methods.values() { + for input in &method.inputs { + if seen_set.seen_ids.insert(input.ty) { + let typ = resolve_typ(metadata, input.ty); + seen_set.collect_types(metadata, typ); + } + } + if seen_set.seen_ids.insert(method.output_ty) { + let typ = resolve_typ(metadata, method.output_ty); + seen_set.collect_types(metadata, typ); + } + } +} +fn collect_extrinsic_types2( + extrinsic: &ExtrinsicMetadata, + seen_set: &mut TypeSet, + metadata: &Metadata, +) { + let mut ids = Vec::from([ + extrinsic.address_ty, + extrinsic.call_ty, + extrinsic.signature_ty, + extrinsic.extra_ty, + ]); + + for signed in &extrinsic.signed_extensions { + ids.push(signed.extra_ty); + ids.push(signed.additional_ty); + } + for id in ids { + if seen_set.seen_ids.insert(id) { + let typ = resolve_typ(metadata, id); + seen_set.collect_types(metadata, typ) + } + } +} /// Collect all type IDs needed to represent the provided pallet. fn collect_pallet_types(pallet: &PalletMetadataInner, type_ids: &mut HashSet) { @@ -178,6 +428,23 @@ where variant.variants.retain(|v| filter(&v.name)); } +fn collect_variants_in_type2(seen_set: &mut TypeSet, metadata: &Metadata, id: u32, mut filter: F) +where + F: FnMut(&TypeDef) -> (), +{ + let ty = metadata + .types + .types + .get(id as usize) + .expect("Metadata should contain enum type in registry"); + + filter(&ty.ty.type_def); + + seen_set.seen_ids.insert(id); + + seen_set.collect_types(metadata, ty); +} + /// Strip any pallets out of the outer enum types that aren't the ones we want to keep. fn retain_pallets_in_runtime_outer_types(metadata: &mut Metadata, mut filter: F) where @@ -207,6 +474,129 @@ pub fn retain_metadata( ) where F: FnMut(&str) -> bool, G: FnMut(&str) -> bool, +{ + let mut type_set = TypeSet { + seen_ids: HashSet::new(), + }; + collect_variants_in_type2( + &mut type_set, + &metadata, + metadata.outer_enums.call_enum_ty, + |type_def| { + let TypeDef::Variant(_) = type_def else { + panic!("Metadata type is expected to be a variant type"); + }; + }, + ); + collect_variants_in_type2( + &mut type_set, + &metadata, + metadata.outer_enums.event_enum_ty, + |type_def| { + let TypeDef::Variant(_) = type_def else { + panic!("Metadata type is expected to be a variant type"); + }; + }, + ); + collect_variants_in_type2( + &mut type_set, + &metadata, + metadata.outer_enums.error_enum_ty, + |type_def| { + let TypeDef::Variant(_) = type_def else { + panic!("Metadata type is expected to be a variant type"); + }; + }, + ); + + for pallet in metadata.pallets.values() { + let should_retain = pallets_filter(&pallet.name); + if should_retain { + collect_pallet_types2(pallet, &mut type_set, &metadata); + } + } + + for api in metadata.apis.values() { + let should_retain = runtime_apis_filter(&api.name); + if should_retain { + collect_runtime_api_types2(api, &mut type_set, metadata); + } + } + + // Additionally, subxt depends on the `DispatchError` type existing; we use the same + // logic here that is used when building our `Metadata`. + let dispatch_error_ty = metadata + .types + .types + .iter() + .find(|ty| ty.ty.path.segments == ["sp_runtime", "DispatchError"]) + .expect("Metadata must contain sp_runtime::DispatchError"); + type_set.seen_ids.insert(dispatch_error_ty.id); + type_set.seen_ids.insert(metadata.runtime_ty); + collect_extrinsic_types2(&metadata.extrinsic, &mut type_set, &metadata); + + for pallet in metadata.pallets.values_mut() { + if !pallets_filter(&pallet.name) { + update_filtered_pallet(pallet, &mut type_set); + } + } + + metadata.pallets.retain(|pallet| { + let should_retain = pallets_filter(&pallet.name); + if !should_retain { + !matches!( + pallet, + PalletMetadataInner { + storage: None, + call_ty: None, + event_ty: None, + error_ty: None, + constants: _, + .. + } + ) + } else { + should_retain + } + }); + metadata.apis.retain(|api| { + let should_retain = runtime_apis_filter(&api.name); + should_retain + }); + + metadata.pallets_by_index = metadata + .pallets + .values() + .iter() + .enumerate() + .map(|(pos, p)| (p.index, pos)) + .collect(); + + let map_ids = metadata.types.retain(|id| type_set.seen_ids.contains(&id)); + // for (k, v) in map_ids.iter() { + // if *v == 96 { + // dbg!(k); + // dbg!(metadata.types.resolve(*k)); + // dbg!(metadata.types.resolve(*v)); + // } + // } + + update_outer_enums(&mut metadata.outer_enums, &map_ids); + for pallets in metadata.pallets.values_mut() { + update_pallet_types(pallets, &map_ids); + } + update_extrinsic_types(&mut metadata.extrinsic, &map_ids); + update_type(&mut metadata.runtime_ty, &map_ids); + update_runtime_api_types(metadata.apis.values_mut(), &map_ids); +} + +pub fn retain_metadata_old( + metadata: &mut Metadata, + mut pallets_filter: F, + mut runtime_apis_filter: G, +) where + F: FnMut(&str) -> bool, + G: FnMut(&str) -> bool, { let mut type_ids = HashSet::new(); @@ -276,13 +666,12 @@ pub fn retain_metadata( update_type(&mut metadata.runtime_ty, &map_ids); update_runtime_api_types(metadata.apis.values_mut(), &map_ids); } - #[cfg(test)] mod tests { use super::*; use crate::Metadata; use assert_matches::assert_matches; - use codec::Decode; + use codec::{Decode, Encode}; use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed}; use std::{fs, path::Path}; @@ -306,7 +695,6 @@ mod tests { // Retain one pallet at a time ensuring the test does not panic. for pallet in metadata_cache.pallets() { let mut metadata = metadata_cache.clone(); - retain_metadata( &mut metadata, |pallet_name| pallet_name == pallet.name(), @@ -356,4 +744,84 @@ mod tests { ); } } + + #[test] + fn example() -> Result<(), Box> { + let files = [(172, "full", "./test_data/metadata.scale.txt")]; + + for (type_id, name, file) in files { + println!("###################################"); + println!("Metadata: {name}"); + println!("###################################\n"); + + let md_file = std::fs::read(file).expect("cannot read metadata"); + let md = Metadata::decode(&mut &*md_file).expect("cannot decode metadata"); + + let outer_enum_hashes = crate::OuterEnumHashes::empty(); + let hash = + crate::utils::validation::get_type_hash(md.types(), type_id, &outer_enum_hashes); + let mut new_md = md.clone(); + retain_metadata( + &mut new_md, + |name| { + let list = "Balances,Timestamp,Contracts,ContractsEvm,System" + .split(",") + .collect::>(); + list.iter().any(|s| *s == name) + }, + |_| true, + ); + let outer_enum_hashes = crate::OuterEnumHashes::empty(); + let new_hash = crate::utils::validation::get_type_hash( + new_md.types(), + type_id, + &outer_enum_hashes, + ); + + // let mut old_md = md.clone(); + // retain_metadata_old( + // &mut old_md, + // |name| { + // let list = "Balances,Timestamp,Contracts,ContractsEvm,System" + // .split(",") + // .collect::>(); + // list.iter().any(|s| *s == name) + // }, + // |_| true, + // ); + + for typ in &new_md.types.types { + assert_eq!( + new_md.type_hash(typ.id), + md.type_hash(typ.id), + "type_id {} {} \n type {:?} {:?}", + typ.id, + typ.ty.path.ident().unwrap(), + typ.ty.type_def, + md.types.resolve(typ.id).unwrap(), + ); + } + + println!("\n{:?}\n{:?}\n", hash, new_hash); + } + + Ok(()) + } + // #[test] + // fn retain_size() { + // let metadata_cache = load_metadata(); + + // // Retain one pallet at a time ensuring the test does not panic. + // for pallet in metadata_cache.pallets() { + // let mut metadata = metadata_cache.clone(); + // let cloned_md = metadata.clone(); + // retain_metadata( + // &mut metadata, + // |pallet_name| pallet_name == pallet.name(), + // |_| true, + // ); + // (cloned_md.types.types.len(), metadata.types.types.len()); + // } + // assert!(false) + // } } From e9ae53b2bd1439c2b71767b194808fcb64445819 Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Thu, 19 Sep 2024 11:46:23 +0200 Subject: [PATCH 02/17] size diff examples --- metadata/src/utils/retain.rs | 87 ++++++++++++++------------ metadata/test_data/metadata.scale.txt | Bin 0 -> 60428 bytes metadata/test_data/metadata1.scale | Bin 0 -> 59222 bytes 3 files changed, 46 insertions(+), 41 deletions(-) create mode 100644 metadata/test_data/metadata.scale.txt create mode 100644 metadata/test_data/metadata1.scale diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index 7f01f9a0b2..d66dc15dfa 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -4,6 +4,8 @@ //! Utility functions to generate a subset of the metadata. +use std::collections::BTreeSet; + use crate::utils::ordered_map::OrderedMap; use crate::{ ExtrinsicMetadata, Metadata, OuterEnumsMetadata, PalletMetadataInner, RuntimeApiMetadataInner, @@ -18,7 +20,7 @@ use scale_info::{ }; struct TypeSet { - seen_ids: HashSet, + seen_ids: BTreeSet, } impl TypeSet { @@ -187,7 +189,7 @@ fn update_filtered_pallet(pallet: &mut PalletMetadataInner, seen_set: &mut TypeS Some(storage) } }); - pallet.storage = None; + pallet.call_ty.and_then(|ty| { if seen_set.seen_ids.contains(&ty) { Some(ty) @@ -215,8 +217,6 @@ fn update_filtered_pallet(pallet: &mut PalletMetadataInner, seen_set: &mut TypeS pallet .constants .retain(|value| seen_set.seen_ids.contains(&value.ty)); - - pallet.constants = OrderedMap::new(); } /// Collect all type IDs needed to represent the runtime APIs. @@ -476,7 +476,7 @@ pub fn retain_metadata( G: FnMut(&str) -> bool, { let mut type_set = TypeSet { - seen_ids: HashSet::new(), + seen_ids: BTreeSet::new(), }; collect_variants_in_type2( &mut type_set, @@ -542,23 +542,20 @@ pub fn retain_metadata( } metadata.pallets.retain(|pallet| { - let should_retain = pallets_filter(&pallet.name); - if !should_retain { - !matches!( + pallets_filter(&pallet.name) + || !matches!( pallet, PalletMetadataInner { storage: None, call_ty: None, event_ty: None, error_ty: None, - constants: _, + constants: map, .. - } + } if map.is_empty() ) - } else { - should_retain - } }); + metadata.apis.retain(|api| { let should_retain = runtime_apis_filter(&api.name); should_retain @@ -571,15 +568,12 @@ pub fn retain_metadata( .enumerate() .map(|(pos, p)| (p.index, pos)) .collect(); - + let mut set = BTreeSet::new(); + for i in type_set.seen_ids.iter() { + metadata.types.resolve(*i).map(|x| set.insert(x)); + } + dbg!(set.len(), type_set.seen_ids.len()); let map_ids = metadata.types.retain(|id| type_set.seen_ids.contains(&id)); - // for (k, v) in map_ids.iter() { - // if *v == 96 { - // dbg!(k); - // dbg!(metadata.types.resolve(*k)); - // dbg!(metadata.types.resolve(*v)); - // } - // } update_outer_enums(&mut metadata.outer_enums, &map_ids); for pallets in metadata.pallets.values_mut() { @@ -656,7 +650,6 @@ pub fn retain_metadata_old( // Now, keep the type IDs we've asked for. This recursively keeps any types referenced from these. // This will return a map from old to new type ID, because IDs may change. let map_ids = metadata.types.retain(|id| type_ids.contains(&id)); - // And finally, we can go and update all of our type IDs in the metadata as a result of this: update_outer_enums(&mut metadata.outer_enums, &map_ids); for pallets in metadata.pallets.values_mut() { @@ -747,7 +740,10 @@ mod tests { #[test] fn example() -> Result<(), Box> { - let files = [(172, "full", "./test_data/metadata.scale.txt")]; + let files = [ + (172, "full", "./test_data/metadata1.scale"), + (172, "full-original", "./test_data/metadata.scale.txt"), + ]; for (type_id, name, file) in files { println!("###################################"); @@ -794,7 +790,7 @@ mod tests { assert_eq!( new_md.type_hash(typ.id), md.type_hash(typ.id), - "type_id {} {} \n type {:?} {:?}", + "type_id {} {} \n type {:?}\n old_type {:?}", typ.id, typ.ty.path.ident().unwrap(), typ.ty.type_def, @@ -807,21 +803,30 @@ mod tests { Ok(()) } - // #[test] - // fn retain_size() { - // let metadata_cache = load_metadata(); - - // // Retain one pallet at a time ensuring the test does not panic. - // for pallet in metadata_cache.pallets() { - // let mut metadata = metadata_cache.clone(); - // let cloned_md = metadata.clone(); - // retain_metadata( - // &mut metadata, - // |pallet_name| pallet_name == pallet.name(), - // |_| true, - // ); - // (cloned_md.types.types.len(), metadata.types.types.len()); - // } - // assert!(false) - // } + #[test] + fn retain_size() { + let metadata_cache = load_metadata(); + + // Retain one pallet at a time ensuring the test does not panic. + for pallet in metadata_cache.pallets() { + let metadata = metadata_cache.clone(); + let mut new_retainer = metadata.clone(); + retain_metadata( + &mut new_retainer, + |pallet_name| pallet_name == pallet.name(), + |_| true, + ); + let mut old_retainer = metadata.clone(); + retain_metadata_old( + &mut old_retainer, + |pallet_name| pallet_name == pallet.name(), + |_| true, + ); + dbg!( + metadata.types.types.len(), + new_retainer.types.types.len(), + old_retainer.types.types.len() + ); + } + } } diff --git a/metadata/test_data/metadata.scale.txt b/metadata/test_data/metadata.scale.txt new file mode 100644 index 0000000000000000000000000000000000000000..7f1975306496e2392ef0c5730fa3fb3c46fdacbd GIT binary patch literal 60428 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 literal 0 HcmV?d00001 diff --git a/metadata/test_data/metadata1.scale b/metadata/test_data/metadata1.scale new file mode 100644 index 0000000000000000000000000000000000000000..a605f4f2f39dccdb865a5a70863c86307264d2a0 GIT binary patch literal 59222 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 XDhd{k9tC00X6-flY?;+>6@vdClG1xc literal 0 HcmV?d00001 From 7d12589e1bf26f3514032933ff6e83903e8a881b Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Tue, 24 Sep 2024 13:47:47 +0200 Subject: [PATCH 03/17] ready for review --- Cargo.lock | 1 - metadata/Cargo.toml | 12 +- metadata/src/utils/retain.rs | 715 ++++++------------ metadata/src/utils/validation.rs | 18 +- .../src/utils/validation/outer_enum_hashes.rs | 17 +- metadata/test_data/metadata.scale.txt | Bin 60428 -> 0 bytes metadata/test_data/metadata1.scale | Bin 59222 -> 0 bytes 7 files changed, 236 insertions(+), 527 deletions(-) delete mode 100644 metadata/test_data/metadata.scale.txt delete mode 100644 metadata/test_data/metadata1.scale diff --git a/Cargo.lock b/Cargo.lock index 04b8867196..555a96c293 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5771,7 +5771,6 @@ dependencies = [ name = "subxt-metadata" version = "0.37.0" dependencies = [ - "assert_matches", "bitvec", "criterion", "frame-metadata 16.0.0", diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 0ad5316100..5512b653b1 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -19,8 +19,13 @@ std = ["scale-info/std", "frame-metadata/std"] [dependencies] scale-info = { workspace = true, default-features = false } -frame-metadata = { workspace = true, default-features = false, features = ["current", "decode"] } -codec = { package = "parity-scale-codec", workspace = true, default-features = false, features = ["derive"] } +frame-metadata = { workspace = true, default-features = false, features = [ + "current", + "decode", +] } +codec = { package = "parity-scale-codec", workspace = true, default-features = false, features = [ + "derive", +] } sp-crypto-hashing = { workspace = true } hashbrown = { workspace = true } @@ -28,7 +33,6 @@ hashbrown = { workspace = true } bitvec = { workspace = true, features = ["alloc"] } criterion = { workspace = true } scale-info = { workspace = true, features = ["bit-vec"] } -assert_matches = { workspace = true } [lib] # Without this, libtest cli opts interfere with criteron benches: @@ -39,4 +43,4 @@ name = "bench" harness = false [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index d66dc15dfa..f59ccec49a 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -4,26 +4,33 @@ //! Utility functions to generate a subset of the metadata. -use std::collections::BTreeSet; +use alloc::collections::BTreeSet; -use crate::utils::ordered_map::OrderedMap; use crate::{ ExtrinsicMetadata, Metadata, OuterEnumsMetadata, PalletMetadataInner, RuntimeApiMetadataInner, StorageEntryType, }; use alloc::collections::{BTreeMap, VecDeque}; use alloc::vec::Vec; -use hashbrown::HashSet; use scale_info::{ - form::PortableForm, PortableType, TypeDef, TypeDefArray, TypeDefBitSequence, TypeDefCompact, - TypeDefComposite, TypeDefSequence, TypeDefTuple, TypeDefVariant, + PortableType, TypeDef, TypeDefArray, TypeDefBitSequence, TypeDefCompact, TypeDefComposite, + TypeDefSequence, TypeDefTuple, TypeDefVariant, }; +use super::variant_index::VariantIndex; + +#[derive(Clone)] struct TypeSet { seen_ids: BTreeSet, } impl TypeSet { + fn new() -> Self { + Self { + seen_ids: BTreeSet::new(), + } + } + fn collect_types(&mut self, metadata: &Metadata, t: &PortableType) { let mut work_set = VecDeque::from([t]); while let Some(typ) = work_set.pop_front() { @@ -77,226 +84,198 @@ impl TypeSet { } } } -} -fn update_types(metadata: &Metadata, map_ids: BTreeMap, t: &PortableType) { - let mut work_set = VecDeque::from([t]); - while let Some(typ) = work_set.pop_front() { - match &typ.ty.type_def { - TypeDef::Composite(TypeDefComposite { fields }) => { - for field in fields { - let ty = resolve_typ(metadata, field.ty.id); - work_set.push_back(ty); - } - } - TypeDef::Variant(TypeDefVariant { variants }) => { - for variant in variants { - for field in &variant.fields { - let ty = resolve_typ(metadata, field.ty.id); - work_set.push_back(ty); - } - } - } - TypeDef::Array(TypeDefArray { len: _, type_param }) - | TypeDef::Sequence(TypeDefSequence { type_param }) - | TypeDef::Compact(TypeDefCompact { type_param }) => { - let ty = resolve_typ(metadata, type_param.id); - work_set.push_back(ty); - } - TypeDef::Tuple(TypeDefTuple { fields }) => { - for field in fields { - let ty = resolve_typ(metadata, field.id); - work_set.push_back(ty); - } - } - TypeDef::Primitive(_) => (), - TypeDef::BitSequence(TypeDefBitSequence { - bit_store_type, - bit_order_type, - }) => { - for typ in [bit_order_type, bit_store_type] { - let ty = resolve_typ(metadata, typ.id); - work_set.push_back(ty); - } - } + fn insert_and_collect_types(&mut self, metadata: &Metadata, ty: &PortableType) { + if self.seen_ids.insert(ty.id) { + self.collect_types(metadata, ty); } } -} -fn resolve_typ(metadata: &Metadata, typ: u32) -> &PortableType { - metadata - .types - .types - .get(typ as usize) - .expect("Metadata should contain enum type in registry") -} -/// Collect all type IDs needed to represent the provided pallet. -fn collect_pallet_types2( - pallet: &PalletMetadataInner, - seen_set: &mut TypeSet, - metadata: &Metadata, -) { - let mut type_ids = Vec::new(); - if let Some(storage) = &pallet.storage { - for entry in storage.entries() { - match entry.entry_type { - StorageEntryType::Plain(ty) => { - type_ids.push(ty); - } - StorageEntryType::Map { - key_ty, value_ty, .. - } => { - type_ids.push(key_ty); - type_ids.push(value_ty); - } - } - } - } - - if let Some(ty) = pallet.call_ty { - type_ids.push(ty); - } - - if let Some(ty) = pallet.event_ty { - type_ids.push(ty); - } + fn collect_extrinsic_types(&mut self, extrinsic: &ExtrinsicMetadata) { + let mut ids = Vec::from([ + extrinsic.address_ty, + extrinsic.call_ty, + extrinsic.signature_ty, + extrinsic.extra_ty, + ]); - for constant in pallet.constants.values() { - type_ids.push(constant.ty); + for signed in &extrinsic.signed_extensions { + ids.push(signed.extra_ty); + ids.push(signed.additional_ty); + } + for id in ids { + self.seen_ids.insert(id); + } } - if let Some(ty) = pallet.error_ty { - type_ids.push(ty); - } - for id in type_ids { - if seen_set.seen_ids.insert(id) { - let typ = resolve_typ(metadata, id); - seen_set.collect_types(metadata, typ) + /// Collect all type IDs needed to represent the runtime APIs. + fn collect_runtime_api_types(&mut self, api: &RuntimeApiMetadataInner) { + for method in api.methods.values() { + for input in &method.inputs { + self.seen_ids.insert(input.ty); + } + self.seen_ids.insert(method.output_ty); } } -} -fn update_filtered_pallet(pallet: &mut PalletMetadataInner, seen_set: &mut TypeSet) { - pallet.storage.as_mut().and_then(|storage| { - storage.retain_entries(|entry| match entry.entry_type { - StorageEntryType::Plain(ty) => seen_set.seen_ids.contains(&ty), - StorageEntryType::Map { - key_ty, value_ty, .. - } => seen_set.seen_ids.contains(&key_ty) && seen_set.seen_ids.contains(&value_ty), - }); - if storage.entries().len() == 0 { - None - } else { - Some(storage) - } - }); - pallet.call_ty.and_then(|ty| { - if seen_set.seen_ids.contains(&ty) { - Some(ty) - } else { - None + /// Collect all type IDs needed to represent the provided pallet. + fn collect_pallet_types(&mut self, pallet: &PalletMetadataInner, metadata: &Metadata) { + let mut type_ids = Vec::new(); + if let Some(storage) = &pallet.storage { + for entry in storage.entries() { + match entry.entry_type { + StorageEntryType::Plain(ty) => { + type_ids.push(ty); + } + StorageEntryType::Map { + key_ty, value_ty, .. + } => { + type_ids.push(key_ty); + type_ids.push(value_ty); + } + } + } } - }); - pallet.event_ty.and_then(|ty| { - if seen_set.seen_ids.contains(&ty) { - Some(ty) - } else { - None + if let Some(ty) = pallet.call_ty { + type_ids.push(ty); } - }); - pallet.error_ty.and_then(|ty| { - if seen_set.seen_ids.contains(&ty) { - Some(ty) - } else { - None + if let Some(ty) = pallet.event_ty { + type_ids.push(ty); } - }); - - pallet - .constants - .retain(|value| seen_set.seen_ids.contains(&value.ty)); -} -/// Collect all type IDs needed to represent the runtime APIs. -fn collect_runtime_api_types2( - api: &RuntimeApiMetadataInner, - seen_set: &mut TypeSet, - metadata: &Metadata, -) { - for method in api.methods.values() { - for input in &method.inputs { - if seen_set.seen_ids.insert(input.ty) { - let typ = resolve_typ(metadata, input.ty); - seen_set.collect_types(metadata, typ); - } + for constant in pallet.constants.values() { + type_ids.push(constant.ty); } - if seen_set.seen_ids.insert(method.output_ty) { - let typ = resolve_typ(metadata, method.output_ty); - seen_set.collect_types(metadata, typ); + + if let Some(ty) = pallet.error_ty { + type_ids.push(ty); } - } -} -fn collect_extrinsic_types2( - extrinsic: &ExtrinsicMetadata, - seen_set: &mut TypeSet, - metadata: &Metadata, -) { - let mut ids = Vec::from([ - extrinsic.address_ty, - extrinsic.call_ty, - extrinsic.signature_ty, - extrinsic.extra_ty, - ]); - - for signed in &extrinsic.signed_extensions { - ids.push(signed.extra_ty); - ids.push(signed.additional_ty); - } - for id in ids { - if seen_set.seen_ids.insert(id) { + for id in type_ids { let typ = resolve_typ(metadata, id); - seen_set.collect_types(metadata, typ) + self.insert_and_collect_types(metadata, typ); } } -} - -/// Collect all type IDs needed to represent the provided pallet. -fn collect_pallet_types(pallet: &PalletMetadataInner, type_ids: &mut HashSet) { - if let Some(storage) = &pallet.storage { - for entry in storage.entries() { - match entry.entry_type { - StorageEntryType::Plain(ty) => { - type_ids.insert(ty); + /// Strips pallets that we need to keep around for their types + fn update_filtered_pallet( + &mut self, + pallet: &mut PalletMetadataInner, + retained_set: &mut TypeSet, + ) { + let entry_fn = |entry: &crate::StorageEntryMetadata| match entry.entry_type { + StorageEntryType::Plain(ty) => { + self.seen_ids.contains(&ty) && retained_set.seen_ids.remove(&ty) + } + StorageEntryType::Map { + key_ty, value_ty, .. + } => { + if self.seen_ids.contains(&key_ty) && self.seen_ids.contains(&value_ty) { + retained_set.seen_ids.remove(&key_ty) && retained_set.seen_ids.remove(&value_ty) + } else { + false } - StorageEntryType::Map { - key_ty, value_ty, .. - } => { - type_ids.insert(key_ty); - type_ids.insert(value_ty); + } + }; + let new_storage = match pallet.storage.as_mut() { + Some(storage) => { + // check if the both types in the seen_set and keep the entry if types were not retained already + storage.retain_entries(entry_fn); + // if the storage list is empty - drop it completetely + if storage.entries().is_empty() { + None + } else { + Some(storage) } } + None => None, + }; + + pallet.storage = new_storage.cloned(); + + // Helpers + let mut check_opt_and_retain = |option: Option| -> Option { + match option { + Some(ty) if self.seen_ids.contains(&ty) && retained_set.seen_ids.remove(&ty) => { + Some(ty) + } + _ => None, + } + }; + fn reset_variant_index(variant_index: &mut VariantIndex, opt: Option) { + if opt.is_none() { + *variant_index = VariantIndex::empty() + } } - } - if let Some(ty) = pallet.call_ty { - type_ids.insert(ty); - } + pallet.call_ty = check_opt_and_retain(pallet.call_ty); + reset_variant_index(&mut pallet.call_variant_index, pallet.call_ty); - if let Some(ty) = pallet.event_ty { - type_ids.insert(ty); - } + pallet.event_ty = check_opt_and_retain(pallet.event_ty); + reset_variant_index(&mut pallet.event_variant_index, pallet.event_ty); + + pallet.error_ty = check_opt_and_retain(pallet.error_ty); + reset_variant_index(&mut pallet.error_variant_index, pallet.error_ty); - for constant in pallet.constants.values() { - type_ids.insert(constant.ty); + pallet.constants.retain(|value| { + self.seen_ids.contains(&value.ty) && retained_set.seen_ids.remove(&value.ty) + }); } - if let Some(ty) = pallet.error_ty { - type_ids.insert(ty); + // Collect types referenced inside outer enum + fn collect_variants_in_type(&mut self, metadata: &mut Metadata, id: u32, mut name_filter: F) + where + F: FnMut(&str) -> bool, + { + let m = metadata.clone(); + + let ty = { + metadata + .types + .types + .get_mut(id as usize) + .expect("Metadata should contain enum type in registry") + }; + + let mut for_mut_ty = ty.clone(); + + let TypeDef::Variant(variant) = &mut for_mut_ty.ty.type_def else { + panic!("Metadata type is expected to be a variant type"); + }; + + // Remove all variants from the cloned type that aren't the pallet(s) we want to keep. + variant.variants.retain(|v| name_filter(&v.name)); + + // traverse the enum and collect the types + self.collect_types(&m, &for_mut_ty); + + // Redo the thing above but keep filtered out variants if they reference types that we intend to keep + let TypeDef::Variant(variant) = &mut ty.ty.type_def else { + panic!("Metadata type is expected to be a variant type"); + }; + + variant.variants.retain(|v| { + name_filter(&v.name) || { + v.fields + .iter() + .all(|field| self.seen_ids.contains(&field.ty.id)) + } + }); + + self.seen_ids.insert(id); + + self.collect_types(&m, ty); } } +fn resolve_typ(metadata: &Metadata, typ: u32) -> &PortableType { + metadata + .types + .types + .get(typ as usize) + .expect("Metadata should contain enum type in registry") +} + /// Update all type IDs of the provided pallet using the new type IDs from the portable registry. fn update_pallet_types(pallet: &mut PalletMetadataInner, map_ids: &BTreeMap) { if let Some(storage) = &mut pallet.storage { @@ -314,7 +293,6 @@ fn update_pallet_types(pallet: &mut PalletMetadataInner, map_ids: &BTreeMap) { - type_ids.insert(extrinsic.address_ty); - type_ids.insert(extrinsic.call_ty); - type_ids.insert(extrinsic.signature_ty); - type_ids.insert(extrinsic.extra_ty); - - for signed in &extrinsic.signed_extensions { - type_ids.insert(signed.extra_ty); - type_ids.insert(signed.additional_ty); - } -} - /// Update all type IDs of the provided extrinsic metadata using the new type IDs from the portable registry. fn update_extrinsic_types(extrinsic: &mut ExtrinsicMetadata, map_ids: &BTreeMap) { update_type(&mut extrinsic.address_ty, map_ids); @@ -358,16 +323,6 @@ fn update_extrinsic_types(extrinsic: &mut ExtrinsicMetadata, map_ids: &BTreeMap< } } -/// Collect all type IDs needed to represent the runtime APIs. -fn collect_runtime_api_types(api: &RuntimeApiMetadataInner, type_ids: &mut HashSet) { - for method in api.methods.values() { - for input in &method.inputs { - type_ids.insert(input.ty); - } - type_ids.insert(method.output_ty); - } -} - /// Update all type IDs of the provided runtime APIs metadata using the new type IDs from the portable registry. fn update_runtime_api_types(apis: &mut [RuntimeApiMetadataInner], map_ids: &BTreeMap) { for api in apis { @@ -380,13 +335,6 @@ fn update_runtime_api_types(apis: &mut [RuntimeApiMetadataInner], map_ids: &BTre } } -/// Collect the outer enums type IDs. -fn collect_outer_enums(enums: &OuterEnumsMetadata, type_ids: &mut HashSet) { - type_ids.insert(enums.call_enum_ty); - type_ids.insert(enums.event_enum_ty); - type_ids.insert(enums.error_enum_ty); -} - /// Update all the type IDs for outer enums. fn update_outer_enums(enums: &mut OuterEnumsMetadata, map_ids: &BTreeMap) { update_type(&mut enums.call_enum_ty, map_ids); @@ -408,53 +356,6 @@ fn update_type(ty: &mut u32, map_ids: &BTreeMap) { *ty = new_id; } -/// Retain the enum type identified by ID and keep only the variants that -/// match the provided filter. -fn retain_variants_in_enum_type(metadata: &mut Metadata, id: u32, mut filter: F) -where - F: FnMut(&str) -> bool, -{ - let ty = metadata - .types - .types - .get_mut(id as usize) - .expect("Metadata should contain enum type in registry"); - - let TypeDef::Variant(variant) = &mut ty.ty.type_def else { - panic!("Metadata type is expected to be a variant type"); - }; - - // Remove all variants from the type that aren't the pallet(s) we want to keep. - variant.variants.retain(|v| filter(&v.name)); -} - -fn collect_variants_in_type2(seen_set: &mut TypeSet, metadata: &Metadata, id: u32, mut filter: F) -where - F: FnMut(&TypeDef) -> (), -{ - let ty = metadata - .types - .types - .get(id as usize) - .expect("Metadata should contain enum type in registry"); - - filter(&ty.ty.type_def); - - seen_set.seen_ids.insert(id); - - seen_set.collect_types(metadata, ty); -} - -/// Strip any pallets out of the outer enum types that aren't the ones we want to keep. -fn retain_pallets_in_runtime_outer_types(metadata: &mut Metadata, mut filter: F) -where - F: FnMut(&str) -> bool, -{ - retain_variants_in_enum_type(metadata, metadata.outer_enums.call_enum_ty, &mut filter); - retain_variants_in_enum_type(metadata, metadata.outer_enums.event_enum_ty, &mut filter); - retain_variants_in_enum_type(metadata, metadata.outer_enums.error_enum_ty, &mut filter); -} - /// Generate a subset of the metadata that contains only the /// types needed to represent the provided pallets and runtime APIs. /// @@ -475,51 +376,23 @@ pub fn retain_metadata( F: FnMut(&str) -> bool, G: FnMut(&str) -> bool, { - let mut type_set = TypeSet { - seen_ids: BTreeSet::new(), - }; - collect_variants_in_type2( - &mut type_set, - &metadata, - metadata.outer_enums.call_enum_ty, - |type_def| { - let TypeDef::Variant(_) = type_def else { - panic!("Metadata type is expected to be a variant type"); - }; - }, - ); - collect_variants_in_type2( - &mut type_set, - &metadata, - metadata.outer_enums.event_enum_ty, - |type_def| { - let TypeDef::Variant(_) = type_def else { - panic!("Metadata type is expected to be a variant type"); - }; - }, - ); - collect_variants_in_type2( - &mut type_set, - &metadata, - metadata.outer_enums.error_enum_ty, - |type_def| { - let TypeDef::Variant(_) = type_def else { - panic!("Metadata type is expected to be a variant type"); - }; - }, - ); + // Types specifically referenced inside pallets that we keep + let mut retained_set = TypeSet::new(); for pallet in metadata.pallets.values() { let should_retain = pallets_filter(&pallet.name); if should_retain { - collect_pallet_types2(pallet, &mut type_set, &metadata); + retained_set.collect_pallet_types(pallet, metadata); } } + // all types that we intend to keep + let mut type_set = retained_set.clone(); + for api in metadata.apis.values() { let should_retain = runtime_apis_filter(&api.name); if should_retain { - collect_runtime_api_types2(api, &mut type_set, metadata); + type_set.collect_runtime_api_types(api); } } @@ -533,14 +406,32 @@ pub fn retain_metadata( .expect("Metadata must contain sp_runtime::DispatchError"); type_set.seen_ids.insert(dispatch_error_ty.id); type_set.seen_ids.insert(metadata.runtime_ty); - collect_extrinsic_types2(&metadata.extrinsic, &mut type_set, &metadata); + type_set.collect_extrinsic_types(&metadata.extrinsic); + + // Collect the outer enums type IDs. + for typ in [ + metadata.outer_enums.call_enum_ty, + metadata.outer_enums.event_enum_ty, + metadata.outer_enums.error_enum_ty, + ] { + type_set.collect_variants_in_type(metadata, typ, &mut pallets_filter); + } + let mut retained_set = TypeSet { + seen_ids: type_set + .seen_ids + .difference(&retained_set.seen_ids) + .copied() + .collect(), + }; + // Strip down Pallets we dont need and only keep types that are not yet included in the retained set. for pallet in metadata.pallets.values_mut() { if !pallets_filter(&pallet.name) { - update_filtered_pallet(pallet, &mut type_set); + type_set.update_filtered_pallet(pallet, &mut retained_set); } } + // Filter out unnecesary pallets that have no entries metadata.pallets.retain(|pallet| { pallets_filter(&pallet.name) || !matches!( @@ -556,10 +447,8 @@ pub fn retain_metadata( ) }); - metadata.apis.retain(|api| { - let should_retain = runtime_apis_filter(&api.name); - should_retain - }); + // Retain the apis + metadata.apis.retain(|api| runtime_apis_filter(&api.name)); metadata.pallets_by_index = metadata .pallets @@ -568,11 +457,7 @@ pub fn retain_metadata( .enumerate() .map(|(pos, p)| (p.index, pos)) .collect(); - let mut set = BTreeSet::new(); - for i in type_set.seen_ids.iter() { - metadata.types.resolve(*i).map(|x| set.insert(x)); - } - dbg!(set.len(), type_set.seen_ids.len()); + let map_ids = metadata.types.retain(|id| type_set.seen_ids.contains(&id)); update_outer_enums(&mut metadata.outer_enums, &map_ids); @@ -584,87 +469,11 @@ pub fn retain_metadata( update_runtime_api_types(metadata.apis.values_mut(), &map_ids); } -pub fn retain_metadata_old( - metadata: &mut Metadata, - mut pallets_filter: F, - mut runtime_apis_filter: G, -) where - F: FnMut(&str) -> bool, - G: FnMut(&str) -> bool, -{ - let mut type_ids = HashSet::new(); - - // There are special outer enum types that point to all pallets types (call, error, event) by default. - // This brings in a significant chunk of types. We trim this down to only include variants - // for the pallets we're retaining, to avoid this. - retain_pallets_in_runtime_outer_types(metadata, &mut pallets_filter); - - // Collect the stripped outer enums. - collect_outer_enums(&metadata.outer_enums, &mut type_ids); - - // Filter our pallet list to only those pallets we want to keep. Keep hold of all - // type IDs in the pallets we're keeping. Retain all, if no filter specified. - metadata.pallets.retain(|pallet| { - let should_retain = pallets_filter(&pallet.name); - if should_retain { - collect_pallet_types(pallet, &mut type_ids); - } - should_retain - }); - - // We index pallets by their u8 index for easy access. Rebuild this index. - metadata.pallets_by_index = metadata - .pallets - .values() - .iter() - .enumerate() - .map(|(pos, p)| (p.index, pos)) - .collect(); - - // Keep the extrinsic stuff referenced in our metadata. - collect_extrinsic_types(&metadata.extrinsic, &mut type_ids); - - // Keep the "runtime" type ID, since it's referenced in our metadata. - type_ids.insert(metadata.runtime_ty); - - // Keep only the runtime API types that the filter allows for. Keep hold of all - // type IDs in the runtime apis we're keeping. Retain all, if no filter specified. - metadata.apis.retain(|api| { - let should_retain = runtime_apis_filter(&api.name); - if should_retain { - collect_runtime_api_types(api, &mut type_ids); - } - should_retain - }); - - // Additionally, subxt depends on the `DispatchError` type existing; we use the same - // logic here that is used when building our `Metadata`. - let dispatch_error_ty = metadata - .types - .types - .iter() - .find(|ty| ty.ty.path.segments == ["sp_runtime", "DispatchError"]) - .expect("Metadata must contain sp_runtime::DispatchError"); - type_ids.insert(dispatch_error_ty.id); - - // Now, keep the type IDs we've asked for. This recursively keeps any types referenced from these. - // This will return a map from old to new type ID, because IDs may change. - let map_ids = metadata.types.retain(|id| type_ids.contains(&id)); - // And finally, we can go and update all of our type IDs in the metadata as a result of this: - update_outer_enums(&mut metadata.outer_enums, &map_ids); - for pallets in metadata.pallets.values_mut() { - update_pallet_types(pallets, &map_ids); - } - update_extrinsic_types(&mut metadata.extrinsic, &map_ids); - update_type(&mut metadata.runtime_ty, &map_ids); - update_runtime_api_types(metadata.apis.values_mut(), &map_ids); -} #[cfg(test)] mod tests { use super::*; use crate::Metadata; - use assert_matches::assert_matches; - use codec::{Decode, Encode}; + use codec::Decode; use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed}; use std::{fs, path::Path}; @@ -687,6 +496,7 @@ mod tests { // Retain one pallet at a time ensuring the test does not panic. for pallet in metadata_cache.pallets() { + let original_meta = metadata_cache.clone(); let mut metadata = metadata_cache.clone(); retain_metadata( &mut metadata, @@ -694,26 +504,17 @@ mod tests { |_| true, ); - assert_eq!(metadata.pallets.len(), 1); - assert_eq!( - &*metadata.pallets.get_by_index(0).unwrap().name, - pallet.name() + assert!( + metadata.pallets.len() < original_meta.pallets.len(), + "Stripped metadata must have less pallets than the non-stripped one: stripped amount {}, original amount {}", + metadata.pallets.len(), original_meta.pallets.len() ); - let id = metadata.outer_enums().call_enum_ty; - let ty = metadata.types.resolve(id).unwrap(); - let num_variants = if pallet.call_ty_id().is_some() { 1 } else { 0 }; - assert_matches!(&ty.type_def, TypeDef::Variant(variant) if variant.variants.len() == num_variants); - - let id = metadata.outer_enums().error_enum_ty; - let ty = metadata.types.resolve(id).unwrap(); - let num_variants = if pallet.error_ty_id().is_some() { 1 } else { 0 }; - assert_matches!(&ty.type_def, TypeDef::Variant(variant) if variant.variants.len() == num_variants); - - let id = metadata.outer_enums().event_enum_ty; - let ty = metadata.types.resolve(id).unwrap(); - let num_variants = if pallet.event_ty_id().is_some() { 1 } else { 0 }; - assert_matches!(&ty.type_def, TypeDef::Variant(variant) if variant.variants.len() == num_variants); + assert!( + metadata.types.types.len() < original_meta.types.types.len(), + "Stripped metadata must have less retained types than the non-stripped one: stripped amount {}, original amount {}", + metadata.types.types.len(), original_meta.types.types.len() + ) } } @@ -737,96 +538,4 @@ mod tests { ); } } - - #[test] - fn example() -> Result<(), Box> { - let files = [ - (172, "full", "./test_data/metadata1.scale"), - (172, "full-original", "./test_data/metadata.scale.txt"), - ]; - - for (type_id, name, file) in files { - println!("###################################"); - println!("Metadata: {name}"); - println!("###################################\n"); - - let md_file = std::fs::read(file).expect("cannot read metadata"); - let md = Metadata::decode(&mut &*md_file).expect("cannot decode metadata"); - - let outer_enum_hashes = crate::OuterEnumHashes::empty(); - let hash = - crate::utils::validation::get_type_hash(md.types(), type_id, &outer_enum_hashes); - let mut new_md = md.clone(); - retain_metadata( - &mut new_md, - |name| { - let list = "Balances,Timestamp,Contracts,ContractsEvm,System" - .split(",") - .collect::>(); - list.iter().any(|s| *s == name) - }, - |_| true, - ); - let outer_enum_hashes = crate::OuterEnumHashes::empty(); - let new_hash = crate::utils::validation::get_type_hash( - new_md.types(), - type_id, - &outer_enum_hashes, - ); - - // let mut old_md = md.clone(); - // retain_metadata_old( - // &mut old_md, - // |name| { - // let list = "Balances,Timestamp,Contracts,ContractsEvm,System" - // .split(",") - // .collect::>(); - // list.iter().any(|s| *s == name) - // }, - // |_| true, - // ); - - for typ in &new_md.types.types { - assert_eq!( - new_md.type_hash(typ.id), - md.type_hash(typ.id), - "type_id {} {} \n type {:?}\n old_type {:?}", - typ.id, - typ.ty.path.ident().unwrap(), - typ.ty.type_def, - md.types.resolve(typ.id).unwrap(), - ); - } - - println!("\n{:?}\n{:?}\n", hash, new_hash); - } - - Ok(()) - } - #[test] - fn retain_size() { - let metadata_cache = load_metadata(); - - // Retain one pallet at a time ensuring the test does not panic. - for pallet in metadata_cache.pallets() { - let metadata = metadata_cache.clone(); - let mut new_retainer = metadata.clone(); - retain_metadata( - &mut new_retainer, - |pallet_name| pallet_name == pallet.name(), - |_| true, - ); - let mut old_retainer = metadata.clone(); - retain_metadata_old( - &mut old_retainer, - |pallet_name| pallet_name == pallet.name(), - |_| true, - ); - dbg!( - metadata.types.types.len(), - new_retainer.types.types.len(), - old_retainer.types.types.len() - ); - } - } } diff --git a/metadata/src/utils/validation.rs b/metadata/src/utils/validation.rs index dfa4f9b829..cea20328cf 100644 --- a/metadata/src/utils/validation.rs +++ b/metadata/src/utils/validation.rs @@ -115,7 +115,6 @@ fn get_variant_hash( fn get_type_def_variant_hash( registry: &PortableRegistry, variant: &TypeDefVariant, - only_these_variants: Option<&[&str]>, cache: &mut HashMap, outer_enum_hashes: &OuterEnumHashes, ) -> Hash { @@ -123,10 +122,7 @@ fn get_type_def_variant_hash( let variant_field_bytes = variant.variants.iter().fold([0u8; HASH_LEN], |bytes, var| { // With EncodeAsType and DecodeAsType we no longer care which order the variants are in, // as long as all of the names+types are there. XOR to not care about ordering. - let should_hash = only_these_variants - .as_ref() - .map(|only_these_variants| only_these_variants.contains(&var.name.as_str())) - .unwrap_or(true); + let should_hash = true; if should_hash { xor( bytes, @@ -164,7 +160,7 @@ fn get_type_def_hash( concat_and_hash2(&composite_id_bytes, &composite_field_bytes) } TypeDef::Variant(variant) => { - get_type_def_variant_hash(registry, variant, None, cache, outer_enum_hashes) + get_type_def_variant_hash(registry, variant, cache, outer_enum_hashes) } TypeDef::Sequence(sequence) => concat_and_hash2( &[TypeBeingHashed::Sequence as u8; HASH_LEN], @@ -588,7 +584,7 @@ impl<'a> MetadataHasher<'a> { // Get the hashes of outer enums, considering only `specific_pallets` (if any are set). // If any of the typed that represent outer enums are encountered later, hashes from `top_level_enum_hashes` can be substituted. - let outer_enum_hashes = OuterEnumHashes::new(metadata, self.specific_pallets.as_deref()); + let outer_enum_hashes = OuterEnumHashes::new(metadata); let pallet_hash = metadata.pallets().fold([0u8; HASH_LEN], |bytes, pallet| { // If specific pallets are given, only include this pallet if it is in the specific pallets. @@ -1235,9 +1231,15 @@ mod tests { m }; + let name_set: Vec = trimmed_metadata + .pallets + .values() + .iter() + .map(|pallet| pallet.name.to_string()) + .collect(); // test that the hashes are the same: let hash = MetadataHasher::new(&metadata) - .only_these_pallets(&["First"]) + .only_these_pallets(&name_set) .hash(); let hash_trimmed = MetadataHasher::new(&trimmed_metadata).hash(); diff --git a/metadata/src/utils/validation/outer_enum_hashes.rs b/metadata/src/utils/validation/outer_enum_hashes.rs index 5e50fcb40e..ebd04f9d13 100644 --- a/metadata/src/utils/validation/outer_enum_hashes.rs +++ b/metadata/src/utils/validation/outer_enum_hashes.rs @@ -19,13 +19,9 @@ pub struct OuterEnumHashes { } impl OuterEnumHashes { - /// Constructs new `OuterEnumHashes` from metadata. If `only_these_variants` is set, the enums are stripped down to only these variants, before their hashes are calculated. - pub fn new(metadata: &Metadata, only_these_variants: Option<&[&str]>) -> Self { - fn get_enum_hash( - registry: &PortableRegistry, - id: u32, - only_these_variants: Option<&[&str]>, - ) -> Hash { + /// Constructs new `OuterEnumHashes` from metadata. + pub fn new(metadata: &Metadata) -> Self { + fn get_enum_hash(registry: &PortableRegistry, id: u32) -> Hash { let ty = registry .types .get(id as usize) @@ -35,7 +31,6 @@ impl OuterEnumHashes { get_type_def_variant_hash( registry, variant, - only_these_variants, &mut HashMap::new(), // ignored, because not computed yet... &OuterEnumHashes::empty(), @@ -46,9 +41,9 @@ impl OuterEnumHashes { } let enums = &metadata.outer_enums; - let call_hash = get_enum_hash(metadata.types(), enums.call_enum_ty, only_these_variants); - let event_hash = get_enum_hash(metadata.types(), enums.event_enum_ty, only_these_variants); - let error_hash = get_enum_hash(metadata.types(), enums.error_enum_ty, only_these_variants); + let call_hash = get_enum_hash(metadata.types(), enums.call_enum_ty); + let event_hash = get_enum_hash(metadata.types(), enums.event_enum_ty); + let error_hash = get_enum_hash(metadata.types(), enums.error_enum_ty); Self { call_hash: (enums.call_enum_ty, call_hash), diff --git a/metadata/test_data/metadata.scale.txt b/metadata/test_data/metadata.scale.txt deleted file mode 100644 index 7f1975306496e2392ef0c5730fa3fb3c46fdacbd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60428 zcmdVD4Pa!~bsl!_@GS{Spgpv}1Wcg#Q3PWwjeW#Ypaeood9e$u7SaBI3oL(3ZZQMQ z!(z098ED?j?n0JYx}_SbUN>|?)lwogaze#YBd2mBxl}_pQcJZ`Bezs4Cvqz{aw8>j zqa<=GHF85I`Mz`Rz3&ZXus<>tr?gDs-FffcbI(2Z{NHo8(oXe9f4gLc$Fl9!^;SC_ zU2pGfcUqHk>+7vhKHs$+SRRe zHQUKL=~no*xzQRLIn`>er?%`H=c}En9Wf(zq{$!3R$nl7e7)7ox?5>GE8F-?x^a}l zqj|gCx>&F6Zl27#8yofYdfM#tzF)3kfDv09&X_X5+-F9H4{cW)jkL46R&7)^6Wen4;Qcek0MYwyG2{5RF%!en4$g>kGp6Q1TWDUaw_D9E zkonndEL2+KmOGC9hdqctPhi$dW@*>{Teyh#&!pJSwV`Oiu7DP%K?@_>o7F6R-i|Es zzraDt_f~AQVC-n8wOwD&K4wQbm5M%0sEBu7JNw?`@Xo@n8${DSl_6A zGa~cG9yE6BFj+vonbp^qyX)XNS!AdEwq31*MLub#{MKE>BdGk@xn`XDuj2I6BT5p1}v4eQCy1fnVQ%mO0Zl7;gYw29Kv)O8cy`gIesam?as~G0?&ks{c?lc#eLVyqcI>E(wh zfotv6p$=$=h96Hct{FPj!nbB~sKD~Wn2uIwlrQ4#siW0qv(-7#YF+4VbIc>v+Ua&3 zTpDAJo@~{+jWjY1JE?!__cs~g=+q}j7+_@0^Uzx+Pqmi3J^_Jxc%{|C40g!dMfCuq zA6aQ#NSl$_wiEJ~_IHb!m}}QNn_Fq8z8;y5J(NG{8$)Kgz=Nud$XvEXu2gO4d$*eL zh0B}OE;OZ@d5xJk+G@AE+vHp3wPx~cvw9KxuDaGp&9Ip{(`t22wK{W+M(Yw@zs;CN z5otYBhHxIKgYYj8*`e+X**karaeGA+x07>^t)4tRfA++}>cW{br_Zb|oH}}Xeqnxf zdGV(gpwn#^`z_AgP8>egZPv0PWLT8b&Bl&LB^G7sND2^7Qebqqur6-Vj>`5nH<_uk z%?r)erRH3gr5!B9=rPfmum+fNrrHc9c(XaItF0d%Z#8P>7BdYLEL_H}tO;I*%-E4e zYaM&t+-gi`Xc~$`H7riw!r0;Qv*ZUG01)I9pT<6wzpa^>uV1Wl&mP(N>9pM<4PP$e zVbJ>7p{WzpYR;rpYa=IAL@X6^okZc5cX1Z!uO@3Gp&KG zPdFqlr$ELqCeha!hNaeDM7YQ+D_9=;lZmrU|K^R1xG(xv--zANQ_q;b`xxUA8T*stZjNVC*crJspR~7fMYBXWZHlA-Yj-XPuxN0Cv8U zK@V=$d~5R6V;HazSKH^o@S7cwV6DE6>16V90idulG+mN0N4+*@3E9+i z4`BC9q{;b2#ec4w#t_fh{v>`BySUb_Ub>OdhN9Oiz6!&U8?c`yRuQB`ba>V2YU7VB zLxliuyOH5N?9@U42bmTp!r)?4lU$tdIp2b7>o82vy7WK>-TThMj`qV~Y z?AJRP93n6SvN9Od`m|RWR5ID2@-YZS>XM*0aR*ZMdwO`%c?{^I=u!K`6Hp$3FQX31 zgzvJ}TE$+27W9mT4yryp^o54#Q-oK4^m1(-Udwn3rYN@qnK(O$yBIn#(e|7^%1iHn znukr%x)8%*vz{gsZ%=oQDkeY%!0xkl>9iOdhl;lOTXj_Yu6oax+-|4f z(A})D1995fpUA50(wm}Z2srI~fo^vf`dxIHFNGIiNMrw|w35ZSCGf&tqkd^h(Nlbx z9nseSyA#ezh-?7i1WPip3`ERj^MLc!Me9w`7v|z82mrk-P zbI;QO$XA~m-gIOK!rkrU>Zih}Xww`^)1_*?Haw82+X;BWx^VwNDH*j>ZV#N^pm)#? zck0`_8_m$;b}lYJL3IY~JM;n;4o&USW_^7#uAl?mwAC8;H90vcTU?IVqmTfNE-f?m zG5KU5lx=`3LIH|<<=UJ31H*?Ma)KAHA0-o0i%n>sG*U$Sh@qu5T4K=cGBel<}U*Zq)8tbnYk0+api)qV6dH-aA< zJ)t{p3CMo;ZDJ?a;Iv{yq}txu1zq$Fe(n`N`MRC1r*O*y?S+mr?kjbbc#&#HH^aQb z8u;C3M(>VcnWyF<%K=*O2mq`dQEDNfJw8-MyN{re)#4Q9v)F%~O|apVUS!CBE-UvH z1pIs@gZmtQXE(n}0yjx5T^8kJcn>e!lvA!(H&aTHT?7jj(`bQ(_PGU1(WltATFyjA zzDV@*!5`>&l&VI_A&-y+&f==e_AJqoSfR(Y&p=m9&A5Ehu^_I87$ z;&{IACy7rYW(RgzjU`G#(ur$bxR}r9W6@^}FA}{OqN3&cqeW?b(iH$@+s zi`OA9U>N8Uc-VFiC?4Q5%9Zy*{$Y>&cG?ilt#1LwlmhzjA4Q)499LTblDen46CV3q zGv}2a-h1O*>0g1co?hnIX$UREKKmTI(I-zqnh9t^31Q#S`1g+6uq<{O;&Wg(R}g9v z@mc80S*+pfebHrNL0QO8WDX&ixQBA|C}j|s#3=Vj+WwFoS0^aganzA%O1sw?D!#Ky zvNwA4yRcC`Uy%`N$<1T8gP;3;gyZNhaSFRKZId#qR5a+uQJ;SdcXk!dTK?)kOah-c zXqSXIAk))rI)A{u$bzsh8ubfcQ4p#^iZsSSw)meg6;GJNc^(7VZwU<@M(*9UbRBM0 zGNPEVi*8$cgZr1*C=>m>{(B7jwdG1UJb@G zj$ytEaNZhyU*Oy|eta#sJ7Hhc@* z`yWHVeA;{h|Nf-;B-FN{a@l;Ucc8~kRI`qQ!?`^)E<0qWs2lh=-wXBxx6S(O>`_Qt zXnuzP1ytj4d8vtfm=|p=&uN&Qb$u=~9DLrG7my`UHZLNvqhvnY3qs{6I(B3G@S{wM zh9u%VHxh<4FAWS)m&x43E}a~Oa7E-3E*kwK*F|8IY@V!wGiP*F?~0Tr<{89l=zsBy ziUI*VwGE#Q0;UMiRgxLVN+@L2ZG_#C$`K(6f&U$K$s-_G$Wrn?80Kpb8jy?f0vRNm zA5GU7GTK^$ZkguE5Rj1UG@&^vG8dU4T#N$frKlGAh8S;au7_~CW0>O}4>W@AT6}zciy^@%REtO>z5*`}oCd<$j zWFG`4;f!H-Cu7q`}eBprejd4U0TI{0EcTI9mZs!BdbtG+MU$E0m=YSL7k!qux zzSTtV7Xjh2obm=ibsr^D7XU=wMH!ZN_Ogv#4{t@hyu&Dc@oVwy`tK*@d-w`A>WD#( z>K`+97{enEBI~ZPL$_IPF5hmYGs=9=uM{FTSG=saf@#+75JQM;kt7tW+IwOiwD;b7 z>h#LO?7jDbSfD3@P@n+tMY19&BqT(tzQ|KzA{qQ9@@r0T4zd{BJ#9Z^`Kk6EUxEe! zEkcw}A{@Oy(di2G5mblw@8vl~CHMXdHt!+AY!Zdv3H~ zGpWLo%!fRJRYyP3V2*HWxoSQ8z)bRF1kS_Zv8Z!+oz_t&H7iWsZyeq0-YgmA#NmH} zdkwQCnF3Ak?ABrK77?F3)dy_fo*T%`UK9WDVLxXdojY@C@zmR9Rfi0q!eB2TOVnZ# zeuu139BX0gbJJ(ljkM$U{v<7f$8CDK%B&qwM#1!tDHKRe(3q77Gd55w4DY$^?D?Vh zghR~*_xKL%_SP2SRD`CT7ttD)aP>BKJIP zLA~+}PkHSmhWGR*Jd`;| z&M0#!3TbAJ0)b5Rm=O`l7oaG8$$W{5(#(u`8Ba6jE4%cG2D2fuDM`uDJW=HM4BLq{ z1aMYmCWY)IB%EIEZvugjz%q~>x zmOKqsp3T<=PH{IRaKzdfnPo`UE6jA7GYvm{-Of0_yH1CCB46k*_%+~15WGef`hPQD zFS>;%``p5l;uikKz$r}1DxFB1=R0sTBbWymzGYYXa4wpx`PL2Lxt!y<+=pjv$?+`D z=G&5Nqmnc8X`=IMiXtkyWrP9Y%hOEbkxNMOfh$^`%y;Bl$1Z2isFZp{Q}7JZr;BR% zwENkq-gdkW=DeyZ`tIMnbZwsZioR1>dbR)1C|x-)d}Fz~UC+L2$L8=Ks87b8D0szp zcRS6@ht#)f@|O8-G<>vmiAL4;5efJ{j2rxa=@s(|x1zOWW8wP0X$b8@W*`3C7nxh_ zt|ixfJR)glqUnzxT5Mi~IaFJQ;&=*-24CN9Dda^kQ594NH4-A|dat!0%&C-aF~=Od zx^qd)UVH#0s7<%6`EJDuRDBB$*?XB3bs89-I-?&Fxw;p7myWl~!>N)bg55ao~&Krd|xm#hVS9gTEK$%zl@zVyh8 zz6(V7;RFF|DVw*uZH88-j$+cs>J8|D2-oe4UM3|ueS1TieP05%=!n2F491!Zd7_iX zN#=qHTRK@ssw@dFJKa1_lL{%cXYE|P#WxO!B!S1!y`#=Sc};}5gId)pPn|RgHIG68 zZzD8=uqbkSp-gRJgt?HBYi@}iJB6UCOV{<<2d9b5$NaUS=s8XkHiNqVfi#Ta2{yc! zM4Ijy{KP#8zPH&Bm*Q~;Fo3OX7njQI@@N8&KU9Xmo#v>BTuj=dqJPGnfIiY)*q~$M7-~ ztPtXMSxk;U0RFX|%_p$CsAK^ZnrI5SRd3ZQ=MwW07sZoRAVO@vTs_=kY#az##l}ci zX^#uK!bhTja3xnmQuC^C1=T2#ZE`+^9U7b+PGJ+eOyVQhDVLytJq*1LI1oM4Zz1l? zMtZppBSAa^evz=>utt#dAaL|6Nx}Xbw6?m)W}v@1u0E~QFFZ<|!k~F{2IRrdj$N1~ zX8P{CWAYCj=%76S?R4Vc{z^OnT6cyBWmQTd(rrd)pu(g z&pe8TBl;p_J45+**k~DY*}UH7$7k-aeVM@Sw3!Q}GkK_Z+HBYk)oa$;8UCtltxwAzUjniu zezXdv+Pbt_gQVJo8N$5ahpkS`x8G%yW#p%al#<&^#%8Kht9j1*sykqd% z6w>m=p9n>lT->QyiFv?Q1_6Ow8S%Y3$zNdxn&Tu;Gu36H=bnT$y9AGRa=Z7Z=#Ow( zc!ds9NOSZlf+3xJo<1~IQ9>EXsygZPNjHLrjQ)*WH5AAoS;3Wf6wBNJ$$GugxT1lg zicfQp5umBG+Q|$&Td-ihgIyvL_g;t}V4X@sRlDF+BTRTO?k9nt^^|@X%*$!G zUY+0(06{r_Qvf}R=jrwVUV;*ns2vD>dzB$@5E&u_KM;A{H2Sz*7U!XAyiW`U{G4b9 zlX4)#`PC&vfr&W3q{pt37l^S!FMe^Nfil1K;Dh&gQlm(YcUoO!1;7g2bM)YQ{LWsl z%tEbbch@_~5_VkLNj5uMjk|HmYO_^KJZw|`5AMoqqH7{Y?YKLN7aiJXSNv||I#IgP zOX%9f6}F}aztvvgOH{0iJM(cXfd=&z2iKltC{)!?hqXW&n;$4pF1ug#DOEmUVJiiv zk#(qADs!6rI~g25$dzjk+Ll>7afzm2C7-5b3xEK+P@yKnLW1(nSq}`8#YI2IuJ@qB zzYm+&`%8r$3ctFYuli?9qCgjp7A6-g*B-_gPVF$ci1UUPzfBJ$QRjIKh77?IPI8y6 z32)7$S1S)Ywu6?mvz{c0`RodM3hKGrc0CHT--F6S67vx#y=2uDI`qkp*6Z6W5fG@> zElu(`)#CyJXXCk;hc>Y$oJtMHZJBxo>q$8)kekZ|@LSkV>LPJg4Vf$56(?=%#{!m- zEKo!SY$oKeBSppQ_?}<8+!+iEX5lbBu>l=C#d0#55a&cC@l~;1Jx{{IF{?(I(*QmW zija&;;nL^3Rm`M=yv$DPL$M~}5XN1g2xE#TNK*TgL&}H4Ss_g*-*t6fNlFm#LmBA9zU~F0T7H}dvQ3&|V>C-F8VP~bUIx<7tmPzU7tet1DvM00I z+#GM8^+tWpi9LlTrUOb)+5*)C%fd;ee_b^I4f%qm^q+$=8S~F34VbP>yn2b34 z58j0zF%r(Q(C{a` zZ=#)^M{pEw2FF&haed6u%``Ee{}@y=Lqco?#M5gGzzS zxqiJeI4d2~3<$r+YlHejK^p`EW3u7(tAw{&rxS7#q5u^cD*Y(J!LF_;K5TL7g{gn! zKV*xJYcf8)%8plkrnKi`IMB`5`xaaX%3bvf_^YJ2LLH)Z@-8c0p9(uDGHcnNkc3@$ zz$EYjDBYvK~ ze`yZk4nc67`Dk;s0jsWtbskq$IlLj@Yi2Ck{?6xGFDxRP!A7CC&?jK zb-}q5GPvGfI2F>r7>W|7C=XWA-MUWwvU0L|*}q>&rd$tnW@Kh4`hNcTCY!=nk7(Rm zYDv*p4@r2r8|1A>QghwWrhZqNnOma|41~4 zlxu;v;Ls+eHDvi#tER_4paOyi!KDl~=`jpngYP35pQo^pngp+nY9b}s07b}m#dG$F zo{*|Ygm2*Cwxn(col47t3?pTpX<~v0^n@*I-+iQY`Ou*i+WzWme``S5QM`fRJEBO8 zn$Utmn9wm{b^>J*xed^Ry+Frr)nIIh4hP!=Nn8~sW`QqDW>&HyhpL#Ffz&zhT#W|9 z%DY~O2Gu}$m0>)91*Jf9IbC^R)Em<{J|Y#eyz)=jCj}YK0#-FlB-SUFi7ce-h7pQAhmDbf{jEWpqgTbTp(w>TXY3SU5rSvhSo+U+x zDs46S&h7U_9AmFwgvx%>3unOKC_iNGAi%(1I6rqSUGlnD`lj;90RZ;^dQIB%?k1-h zQBykHrn{}MeYobyWPfes5Vk5{WqsPWq1Dl=gePwpm zFT!h(8jbGh{@{O)wc3X?3_2OR(ZNs5c!)nm<~D0T^TQlI3{`-@7K@UdwD!|KOaPpY z07pQ;KqzunxAvny%(0iT9Sh?^>np?+-uXkud>E<#0YVk;+&ff}3#o}i`pq2 zT(3vI_Z){EKVQvOC0`8@_0x@-OEj`8E(00apU7 zIoTs+0qPcnU&Dn$pcK|_TJUk83B^)##;oxs7P-7EL1?uSY5O|55JJJ!Nr zQ0FQh%}teWYxq$#hYI0fpi9nVUc-e z2ehEV!M2lv4ZFSk3-H0$Z~`FwuBVylX!pC+$#;SA92IwNTZFsB)y(pzUiouu5}k#=vM zog(J9xH_Mx6jmO!G*Ro4RX;$jfT?+oPHw=act_j}y^tA-Cr6eSy`!z?>>}ofCg4OI zUUV35Y#{RnGuJE~Oe=XYj>RR}!b0u^0C+LJskORmAS!>V9mNcqJIqLef>xkKnC81E zk&|_&%WEPSWGyt-dhF;q z&~!ylIxrvwUC2SxD2qqpqV)@&vkttBP?4mk?vNeqIi_q6SVK@k;exKh4KB$_aAQcC z71o*VAsAoK1u#6E8p&i~Yxf?d>0#4R`2l-Ij==-@APe#w?l24>M~8GXOLb7)j)->Q z2YK1EAR$*Ix?Mtjw-VsG))iNe(-07J_m;!0tl-@*zo>?t~g$ z4cIvHJt@<3&HSv~H6y5d3aWh*x7wvPY9``*G)4&aaGvt!`~iX+di=Q);W*-o1wb%u zF?^T?MlOn&Azqb(?tBke6HgVq2bdgqtjlw2qGm;w;ZR8)egi3=5f+$zizCrj$S8|D zp3X(85E8c}_^~3%;N6PfPh%BM=*m^GSr$f&1&Mw_% zU^b8i*isVO z+``*dG^!G@od593aN*R#t19_-Yr;1l-e-Brjea2tPn8tjO^8sZR3^d?;Qr2&DP=9> zxZ;||krFd?cf5okCUOK2J~kh_H-vnhce`(au;(~h@(NxRapa z2#`u@DM%gs4V1|&>;UoevCLAvO7QMd5I`P3Z5IT-rVLHRu&Y& zT(?m?STG_SPU$u=_W)+cq@ClJq65J(931W($mv^s%r54WWi6DWq0J){?HKbXLISO9 zZ*VA85j@}aO98ej7#}pOk1aY_=xMuJHwy|qwwwoEcD^zgSsoIjf+1Uz5p|-03Cb0D zm$DNTX&f7`Ox|vp8l@3V->IL0dhkW+Tm!I`fr9lqtdt+xj^#=fEwtTRxX&`Vr3TeC zDC)Ac=>!ZU5kir>RNlQd`*iPSzsvDt=Kz&N9Ugq(s)+`ZZfqb^S4?s&qG|?eRja%d z69+oxPd;P^Xfx1Z;v;w?^#HP)f1M;Z_<^_*Q2t z(sV4#PgEHTxEA&$wPc85gy^97A}*19_eR+qiPmf1$wA@L@K<0+=(ymsP#=hLFT+J_ z%UUNeFsKA;2HPy^<15=-b+1xUelFi5&=bf!b2iAR8k01#S!C`WP;=53f6`g=eVt-j z)38FdN3+4to(2?5C>}(YDNtS|69tds(Jz!(Klgz$g(&cI3~H4d#f_YN zR*~nn6Tc`36ED=rogBx8DdxUVR+D_e|OR}In=C5uCC#d_wODa@h<7m3Jv4|S%E`M8NB*^IyyLk|kA3 z2e0TG+_&H!byQGiRsc5DMHEP-SR)DMr{AOq5t{04sTmFY(}nDaEky+AQ^5{GQlL9bp7#o>-!&T?&H`v~>Ieg%#Cy5H6htbNgs6h^J9Qk?sl=P8 zI*KU_p5dz#&+6LZ$>*uQlh3$!2-ze%140UA(r1t7(GJDl1BDnAj>w45g5SSqV9301 zj5fkK7O8Z>fC}9EPb~{$3vpQDg+B#06KzU_K8O(cQSqAmtR3#XzhDy8QxFE<#sSFA z_%Z%{R_=i+TnY7FJ9L$rpD>9&cKnfpb08n=8BB8Mk^4En_ucTZjiE@6H_hay zn|^*T93yL>ma<$D4;=HBFY$C3J2U4qo7|a+G3-hnb)D)f)adR1DR=DqYW`!Z!laQl z-O+3F3j-hQOC&wfUzCJW=lcB@5hmp75vFFJ9~3aoYj_UW2>TK_~YK8RSMYn z)}`XR-g4+Gix}SGx7U74$_+Tw_4T#loullBG|8pUK4|Uhtvj`KWChtQC#%~iVp&U% zYZ19gej?qt#nvY+FT9$|b zYBmpd(=L$K1kY;=S>viDh_1BF(o;yOO51+PXSe`3%ir5>kFP>xH*r@V5;l+*-2h>Pf^wg;{pIc^3sop^o%2O&JG);#qFyf9ii{=G4fq?@eqr}}nxfTv*&4VT4o4#p z&5ezZNBMEu{<5`Sx#7=&67(?6g^<(ffOUI!Cwd;;gE}QglvlN^)^Az+%TR%Sl!-sZ#2rP5&nlE1GyQ6sbH?H&DX}tU8>wNb&3B_;rr1H92@u;c- zujb07mRxUW%x_(1k7acD+XLR6K;4i>d?k|z^*ta758~p)-x;t6dJ<(xkoMV7<-_H0 zf~4<+j|NT#gq%WRr&oM&+=e%iNM@c~-lN(t&73OjHw0xTNO-fC%)59s@b z*+&yVtU-J(cVOmqjG-D z(BkgT5_fsz=B~wxyC6W)y-M`40{FUt~WY;fsYuKvew|a;#okg zPzwzaL65qWP)M5$wLQl))8OZI7N;J{$&>Vx6OhY9J!Eoc6UT$Oz-HgjT}4=&Yn|{z z0gwXH`KU5CLJS1~nV3lJWf!8!i^zBm%XUcuiwNavEg1al!EyAI9QJzDsaEG0Pr@Vd z_eB)X2n)o)H~53ECz1E%ZFT5kwG?Y~f?jNM3$B^s8qqb-vosZot-hQWlqgSyCh4E&iov?b@lC4r);7x*17a+) zqt(|&`>{hw`q%u{Q7n6xvNiB_(GLPb5W40?NTK{Wb_6a;teN5H8BY$Re75;=7uSpejRrooM+kuog874V*f z+2D`gO-~Xv&;tdeE3xBDAO=da|!25u_ETjh8gt5R+H?EP*}=2 z*-6VTZNq&)tgAa8SlRs}_!eXN*-0y$iM~39ev94>L2?&|dl*b~wP#qcMsN2dSz{J( z8Iu>Hr1)mOAXDhBol;W;$sKy^=t9n@kL|5K7ZSEL;q0k$%buk%vRHD@4z+c*#eAeL@l1VL~n>RSnM z$qY4P1Oa*+hG)mH3z_wT6TcvDac~9jba^y)1bBHd03dQ8@HFxf&<*BE=0a@>7jlDS z=9gK{4djpDzC+s|k4_S8-i_)Gi|;}ommP-6J#S(xFXnM{zSBB`Ggru&Xh>L zLD~%8!3tP95fu*&baiC zzGad!e)P3M&snI+7wgsd%+gUrOElBZ$6|a}^n_ypWrIt`92h3MLB18M?r}mGgF;-E zS=92E(CCw^B>f8nfFdLS5A&0IccAb{!CQE@)K(Mz1$7+=U*XZ~2s@kD=Is`xMi{ zF#H4z7g>Dnu3rB=GvZLNj++74l?$|8T9V_vs$n`MLx2GaGqNE-iA(0X=b`K&%LxP} zde0fSa+=`J@&_&`;A zB3kJKt3eETAh5dz5AyE*LFP2HyCBCk%^RYgZ5lKT9hodi2GUbq{bTGTPeb_yTyr%P z({F6xscuT|}tUJ)rfR4z7bV}HNw54>P=W`@W#R%7Nr16`!ct!J!9H8c@ zE}qj*aWmQ2Lhb$s9ys`5WPaD$R8MiqjWo7;u9rX9yO+b-OXsrhV@CQ|x!l`>E$um_Y3+fxQ)TsRNn|v>yr23fqr{X9cbto)x;=!?S|-W8qohd!2t~ zF#y0j5}p;lJHoTV_s7Ds!uQ9+v%)tTo)x}3!?VKoC&IJB_b0=%!Z#M46~5Q|=Qk>R ze=0mHd_NwZ6}~t;D|~l_XNB(#;aTCkJ3K3Ve*@T~Ct zRCreS4u@xjZ!SD5d`H5w!gn-0D}3|eS>apo&u>=vj)iB1?``2(;X59l6~4vrtnmGr z@T~B?Jv=LXC&IJBcQQOHe5b;*!gty~&nSFL;aTB(I6NzSXTr0>w;Y}ozLoH-@SP3M z3g08)S>by$JS%*Ug=dBDasT|F!uQkRS>bykJS%)phG&KETzFRa{=M+5@U4btg|8Bx z6~1bCR`}M!v%)-498^e~d4odQ${Q5Y?Yu!DeP`aFkhb#%g*3|>6w*%KppbU+28Hxu-k^|P z3Jp%ET+SO5(w)3PA$?cgppgDd-k^}aJ8w`(-;*~er2l^2ppgCtd4odwRNkPF{%mM) z;^jZg8x+$2C~r_mpUxW;()Z>K3h95GHz=fkHg8Z!pUE2(()Z;J3h956Hz=e(7aE+P zxso?1q(7fGD5U>s-k^~Fxx7IkeKv1UNPi)3P)PrL-k^~Fg}gx_eJ*cMNZ%hCoXGjl z@&<+U7xM;%^aFW=Li)kHK_UH%d4odwm+}UM^h0@rLi*vnK_UIm^9F_VFNX#vd_Iyl zD5SrXHz=fkC2vqj|7zZ#kUpO`D5M|F8x+#NmNzJ*znnKHq#w&06w;4}1}BF8i@ZT0 z{p)#yLi&lkK_UH>yg?!T8+n65`d{V^3h5{F28Hxj^9F_VzsegF(!UuRoIv_i-k^|v zI&V-&e=ToNNdN1+K_Pu1Z%{}-lQ$@&|4rVYkp8W_K_PuHZ%{~oJv2Dc^xx(U3hCd@ z8x+#d<_!wz=kf-H^uNm+6w?1bZ%{~I${Q5Y&*u#a>Hm;7D5U?RZ@7O-IsX^(28HyC zd4odwKjjSy>EFp46w)u{4GQURF5{oTAlA^l(T z28Hx1d4odwn|Xsm`uFk%h4k;|4GQU3^9F_VxAF#s^nc466w?1aG&mXcwY)(g{q4L# zA^kt{28Hw=Cfz$IcIWGwI|q7TMkE_-L~_1{ zMvo(KmA38cOXCYZ;9>77p~n9xZt=t~uP<5mjxaTe$QXP5z~Nj! zu9KlrO*6CQ@^QEtnTL!_L*7>;2Lz_=6q{#fHC+gaa_Y=|!Mg7Nghb<3S=6)d?L#?$ zgI^e})#1MH9I~VH@`o{Z4Fvi`Ui2(wIc92GH%z@I-80tZ-Bd~#c%&N$A@|VyvU$Ssu&>SokbMz0C`jYey06O&e0D&x< z2l4tDiRSYnjc1|-`w%`EI(;EB&j1nwFJ3z`59v1#m5OyVwN}5<=&D4P;4Z8b2p4%I z^%~kF`5yBeb9ri!nJZbOjLJ8K_c7eq+2#e@x6_Qw0$ydu&33@QNC!aX){;~{mdi8H=PBNDVo5Du=$al9ra(c0!U29v%ff(IgH zcuKJ;Lncn^SvOOUsxO4HbQe`GFlVf=ME-n-v{KB|oJ6)79mI`Aq#R<1Vb#R67 zIQqcl(1@vdjGKB7x0&f~J51}wIS~-maXa=zcjhhe^f{axDh*%dRF12Rdq$Jn=iPZ`9z+?+Z@n2+ zL2V~_m7){i)-|F?pWIMYvD5x*xewEbY?IUJC^9}8E}}8!XRiaG30w#?FOll3cp1@< zotA=sloG4x_Pr(Bai!3L^uU+CEE+bR9q!b(i=)4{2kGzYNBa8|>F>J^D6yTK$ikpS zIESqqF(rtFvF#Yn80>C4p@qXkh0$HVo1gE2ceIa`kX8;7`Gr!rxy?ssvkB}DWT4^z zUN&>oWs)f07>>@^_m}eZom^lwpfkwNY}f3P!2up5FGH$oVTSyup|-m{RV%a~a!Dut zpwKUs?32qqSwLY)jGdN@0Gy+ce1d|I=0)#hYws`F=nT{!77_|BML?}77kML0j#2j}Om4(AH#M2VvS=8&Aoq`hOO zb4CPsi-#P;J2;$#)DR~973uu*Jy1gCF4!-6Exld<^NaW!xcK7L;XL7aB=7*%PUazJCG$2v{q@G+rNx4? zDNc+zezvA(zfgj2*9W0O?0c+jxR)~=Fz$Qtrk;|nzhmuFyIE!5C#b9imdHiI z-~g=d-_hGt)iGdRKmEk#y9waZ-zt=&xE z_$~`9mZOFk+x=~!`Uf8t#=rp(iDCb|_UV(ME@XD->!-9FrIk#AsqPVrX}5v-W&K*@H@SA9krYc|Slv@(&zIdQBxgz0}LR+o-{w+BpA zU*NT=BxwJ;L#a5ql2OtAGO#gZ6MZDsCuR+j-!db35F!&?&KaLNy|SP;5b>0_Xf&Qgfmj<5C|Ke?WO+cPfcrpo$WXDUk|Pm*g5Vo$-8&$LHOHvy4vIip9+wEj1|KY` zMN2PMk@(BI3c21w+-de4|K0_|p#X%30(rtQc!V26JIRy<^`_}Mg`d~M3D=Z^E65Qf zs4ja`;MAPw1xu!eQbL$3?q)l%4n79ep3BiwwgUvgoOL-u5fa&{OkZHXTPc4>eO{8+ zH!v||=wh`nFaO;Ju4uy*E5JK(T9~om0saa2KNDOTBTI8pt~dc-kq_( zCzZ^Yy?hzB7W<=;NQ=INf<#(I2>J88{0X(F)qNv8x5qfC&y3V7B;69 zxGTqu`4qUuSsT-|AifE9F+o!cg=!OTKEb(Ip1{Oh_-cdKx;ffMHaf08m+V=bv4dkR zS#g8ZR$6D@cs~pjw=XE7A%=`)sZ6Iu1bd_fAlMkd{kCs4GV zCoHx>{S=M$cGlUS#Pe&4?r$FkpgsS1#lfj#0M_pt>KUv8kso8%edvE*hmxX)*pe}q zx8S> z1qb~8SJ>^Xz)*hUh|T{T8a~y!gb+mxEXqI8M`Rax6_2Gb_E6Er!`=462+Lm$76z9f z)X>N|i5x8QNC6^cC$O}-;#CDEPbqDKc<~-~MTK>`D`)}6aA1`hFbpgFx#kLdbDq#y ze8CQ?DmdDNHBX=vK=DI&fCi#m?^*IE^Px)cOP+9X8#3pgZp|w60ppl+Lyly#&R1e@ zL$GoBB)3R+3x{;-t=ecz+T%Z?SCoC|uRMH%pMzc{bIlT*|B8>`urUrr14X*}01^Mg z6{ryqsEBJ>|zkB-Y(rTDsr9Fq8Pp5I#EY(b0T zp&I-r!|!?)L#4bN9DwSClQ^yR1%E=9IBQics92B10^XP(^Oiw}Eg~EO6DnY;mpV>* zQlG=MkuV12a487WpW0)Uc`3Yx5P+W#*3`*bbl+jMY#<1so+Hj9093iZ#&BpGCa}ME ztcPAdAZXW%4ZBSqR^k<8p3IY5HG$%Y5?5#7!#*D!Bws?;(m1Q ziO}QC-#-dT11$>LBCxMi0NX=*3=5&Y4LR?N5~IO-SN0~Sfc=?ELMgUY&H+8gaA#Ee z`c4=LzL3#$HMd(7t9l`xL-lLD1{QHrT8b#dNHGCy2OPNFk|p+=1zNAA}mJ89?&gw+-LneFyJ<@PUK(RiH1(-7Pxx zyD;_P=i^hbnF_Z>?m27os{vFR^K!N1RiKqyB>xbwj_`g-1@r&_jT*5ua8*1}h6NdO z*GbBX&hhPMVr|NovhO2{i^|?z9&o2 zPM%#}iH|JAZ#%Ovx3X|1UO7H@D*ow(GpFyOMZdTa^X~V1;jMwVyjRYcQ}IG~pdy}O zoRk-+R_p0y*~A;EoX?CaF!*4(wNpLrh-*T~cOhftxUmv{0&HD^g^gOl914{*VX{I$ z!~0`Fh8LUrB<{LH0$VFsE#1~_B>Q-7$dG4&K1lSoxxEkS5B~R>=}g#s30B^^BuUw zI!MdG?rf&B086I8bFKi@?G5fL(P62)7U~wq^(J&%spcn9wr)A8_oG-m9&LxT*29J1MYhvrxK5guK>`mtQN}jXh z64dN~wxRh`aG4nH1wGG4)166e&3nPxV4yx1Ag zb4gv*Mo&jWky*ihonuL3RMTxBqlEp7VpjZ!C5J)`0-KHc9qT2(;UetH1npTtz5^HmJkU}%NG#KlV#Rm9?9l8DIn)*4+^*tNUHaJACtAZUM=CD7Gc zqan5}aKXLJ`=406MQ@A9!GPp%Fz`i8WXVaf(&XwGK+F}6E2n5qqgEb-q0hz@{SWT( zW`_<5yp$pL1SWQ4=Kvk?xGA}S-3mlNKvg^;iWi)mfNeiEKI`T4QcsoW1X%Hbnhu-} zp-=^^fY!tuqB7cs7j_b!0%)LzH#Dp3 zxtACyQghnWSRqtGhJHYcq@A$Tm_P+yQ3emZNr~?B#e{|dX$jXV*cF(vl*M8Pg;!#V z9oUf`oon5#ZSm;Uz*3-22(9*DsG>X_pbJb@m^}3jt?&c$2c{p@F+c{GHO@(}d#Gln z2mmZml%8I3UNG6-`wC%Zn_nV2Hnjx74C^&zCW5ii&C~CrI`bv$Xy@PqkApPy%J@1f zm5WMk^bV33kEkxYHZb(a*D^uitn_An}b>G9p>?MPvCfMAd`^KnbkI_ujkJ z!YZ_y^>yC)#F#SZA6P~GA*>yC6)>#^1keW!#^r;EfZUh&1h&Q3s=a|X|axbD(X@l$l2nsfM(5Rd=TZ@ zglXVqR@3otCyn@i1g3nIJR#Qb+bi)KQMqUf`a3TP0{P<}Q0;_p`PcG&Bk>!jsiM>_ zZ%+$LxCU7L{QTEgIMM9Di-~pmnOK;FtJ$c&tBwJwdf{#S-~&03KC}%nE!iZ11A2V# zg5w2Vy$Fs@r;d-*6lX1~B{M;YT5Cd~U(lChE!dgRQ&5b~`A;2Ml<2^fIRSNSg}8Sp z8=9{YoT3*|??~i;9gBzl+Mtnw>n=zgZ^cg)!9w|{yM@=9hb*;XazN}IXGY}SHV&D` z4?IbrR@Nd=-y#ZOa=^tLwrPEX^=JHxTnI)H+N~Xg%R;tWU@QAjEgn{+y1s=Ep}3~M zvr2?z-_77J$19JXe(Zn-QUqp1WMu+!WC|VwK>|~&Y4zY9HzNgxE&8)u^3NWrqF&8lz2Q`k|9h1ezSsg#%GGR(f1Y+ehGr z8I8^Fd~YAW9D!B_N+{F?^`2Kuk?!rb1U3LL3kh;HG?;)+I7f$coIe6hVkU*#LS)_5|Xy5 zOdSU&qC26xoljsLGV<3gDaDjUnxYzJ3Xcfl^0@@`0D2?|hjN|zL%gYxgRxW8<#zXi!I|PDiWMqh0xHdSjbYm+?k!FX9Jn;T1PFv(_*J` z4qOoX2+l(0wtzNeOi_z;+1wqfQW-HQV+bC~WxUlj${Kuzf9vrv?ACVrlJ63v=9qs! zGGri!iG*f8QHvNl%=~YoXxL|Er))VZzZf_V3`lwa7m~ccT{$v2R(-Gva4~-p0=Fuu zIiU!um*AW|6oEFV+>3Cb@9(I$%!gAUhmfAeqyW6y$%*>R@lEc8* zvTizfDFkm%f%u@=ub_(_cv$xkhMeR{dUR=zZqvelktI)}!};Ac>2T>uxg4Y4A+VZa z$GI-iPgVE`^ANi`Un}q<5~0w;%Rc~}CQsrRTy-PGzmbH{fJFntVgoVr@yL#Yuktq3 zT%ZyEGHqYWo8<;Y#$m-OcR$f}y_`eVkgq(MP=z`wu?^h}m;uSV_w~HPpf<@sSUYTC zy`T_e-)-{VOK<{V%aHQo()7V+C+N^DY9L(|F7+1c-Au6+DNbtXMd))-yQoa~y>}UG z9h(WUQf~=UVRk-YSeOjjJz8xy!9mPtsS*17cY6S!8iQ@`V!belC?T?BUT6TNGqv!) z?ncPI=%t>;&vzrqRcmd!lIxznkQ~^AW1!U}oS#F6;XR-q=Bo)|tliUx3iJp7OHg|$}Us?u*z{WaCpNRAAlE?Y{Xk7A!- z{qdr(Qu@LP&UeIEmRDfMr`5LCw7~{CRffE9l>)sAkgm|^f~%{7otYGFn7{=T(1CB& zjpEWnyl!$F%EB?Jzv}k}RN6p#T;Rg=6BvSf4rqVJ-fW_Wadi2cIH(*CsBic^d*!g4 zy)0BKM6ttN)dyL)P~LI{1#M}1SB1cRrRl=i`P)iYibvpYFI}lX_ZWD<{f^R=k4)gO zO*XFK25A&$KxXj!DF zy?X;&q&fk}8Py=0b{9WTOvIJYcaF(Qf~!t+l~1}Qf~4RIw`orb6H^56j&fxZB2tUH zShE_BbYXkqVyi>+5$;ek)13ckJp6fA>A_OD5zp}jp*&rN@O#2MF+WwzAUG+BOJ)MV zds0W%5z}Fjb4!82Oij!;bU|JzpV6&eZ=TSfbu&P>02@tad}fmFBGiX~k#h{23t5Gn zmQUZ(U58t+$WKuSB8-tm=vf;VOZSv`!hyVn;pgx}4v|=Ug&!ofKq%`Xa|=WT6|qFZ zkv|ew(bNx;4SDt;XB!twkC&J(DNGFzgVG6hcw78d!otNK4?k$W!_`NQ-sz~}2$dMO zqop%`~t1XM5RW*MD0=novhJJE4?7oJKe zJaBk{x$S}%7Cw%s(?uLe4`REc-W12UEBWLrkn89!Lt39!YG-$J8u_l})psjlVCupH zs13ODG1@m>p{Ig>QTC893C6C$IG}l90qLu87!xc7H-_X4s^cVIuk88Y6xGS|t_28u z#vNh*sy|rNyF`-B0d{3DJroI!bfk(A@Ck!3NVLMLIRDK2>5UE9K#w9JFKy#WtmqK( zg`{bfNqV@9FNn-5(HD@Hg@ZRr#dz<4+lAHaf>5t6zF?DnGVrzp(wVF8JC)2oI5WU7 zlgVYQF4Qm>DcR79_u`{Cv$U?#^oYtCoD%iY%-9_TjzPzt1X$dwjIz3nr`EQHJi#s1 zROV7ESnMfrDwuqb8-ldHfq5dzsat%3jZ~?T(FoNJ_X9YQbBLTuszX3|z$q^CNtq)1 zmqZPqM>Kw%TM@0W((P=_RNzs=TX5j+eos&xW} z@0Lrk?QkIr>2KeY01#xb3NV=3}V<_IOhy*MUEUbTIx3pb?F*URT|k4visY0pce!!`MzU zpD&HP7(c9p>{(OpZ4vhGnmquL$L*E8%IOOeJVi6X`VlC{vmZVn(di&ZT{l8qU{Z(^ zLzuDDy$Ddja}x!Z_1EDD z6yk{VE$)P)pBzC$R0F56X;Z*nry&SV9laPfzSqfX?H$R-2%n0SbKb>d##yUQ1sk3e zT?&f;T93-_z$u8C-zbek(UBK$07}gz_tjBxE5MzmvGYH(eEJj(!ZjB*6N-RTb{XXA zg;5DXm$umG<9-1WTp$377o__`ebxBbnYoh-2Y5&Zxs*D#Gn%{_h~AMksrIVMSUJ{x zHAr*umP3psd>pP@WRo-22OO#I&{0QNM5mJ0;`~cd`<&`U-!aSry`%>MTxjT11rSw_ z+o0|TuP0&(Jb>S`ga$574b2jZ6iraP2MjCM#B))h((!2Jo@-Nw8ytf*TTkW-A=!}yJGh{sA4Eg-+LudE9YE%O?^FwO4JS@@9!|+5&2$a`j6IMZj=WIK8-+i%RuShQ zHU|IZ^wsQ@#SaUsRGnROct>7~cCYcFMBosH*IOMKVM%+HYeA?(w8vCsk1i7fr^RW| zQCp5G_krwic6D@NO_(G6@Sbq%n&c;jL`x>dgf%#n)e*?zHF54`$o{ixcz1CHYBCuVYbp4hJ92#R7NFqzV2S>;M+<~K`ataiS!ye1ho2(U&&BzI)W*4le# zY46acF~=1k(>*IgCU#{6=cS>7Mq5d6^+ctY^Uu!aTma4`HEGHAEE7bOQbYG#)WDLQ n6!WVWM#TyuHLJ6epxTDhM7`)y5Y7ycCA*zITXyBQO6K1H$cr8P diff --git a/metadata/test_data/metadata1.scale b/metadata/test_data/metadata1.scale deleted file mode 100644 index a605f4f2f39dccdb865a5a70863c86307264d2a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59222 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 XDhd{k9tC00X6-flY?;+>6@vdClG1xc From 1ea20681a6ce1ad151e179df9608e1a7d103b78f Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Tue, 24 Sep 2024 13:57:34 +0200 Subject: [PATCH 04/17] toml formatting --- metadata/Cargo.toml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 5512b653b1..1fa95cead1 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -19,13 +19,8 @@ std = ["scale-info/std", "frame-metadata/std"] [dependencies] scale-info = { workspace = true, default-features = false } -frame-metadata = { workspace = true, default-features = false, features = [ - "current", - "decode", -] } -codec = { package = "parity-scale-codec", workspace = true, default-features = false, features = [ - "derive", -] } +frame-metadata = { workspace = true, default-features = false, features = ["current", "decode"] } +codec = { package = "parity-scale-codec", workspace = true, default-features = false, features = ["derive"] } sp-crypto-hashing = { workspace = true } hashbrown = { workspace = true } @@ -43,4 +38,4 @@ name = "bench" harness = false [lints] -workspace = true +workspace = true \ No newline at end of file From 00a18aba192c2001fc363829c129018877aca52d Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Wed, 2 Oct 2024 15:57:51 +0200 Subject: [PATCH 05/17] review comments --- metadata/src/utils/retain.rs | 27 +++++++++++++++++---------- metadata/src/utils/validation.rs | 13 ++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index f59ccec49a..ddbd619757 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -4,13 +4,11 @@ //! Utility functions to generate a subset of the metadata. -use alloc::collections::BTreeSet; - use crate::{ ExtrinsicMetadata, Metadata, OuterEnumsMetadata, PalletMetadataInner, RuntimeApiMetadataInner, StorageEntryType, }; -use alloc::collections::{BTreeMap, VecDeque}; +use alloc::collections::{BTreeMap, BTreeSet, VecDeque}; use alloc::vec::Vec; use scale_info::{ PortableType, TypeDef, TypeDefArray, TypeDefBitSequence, TypeDefCompact, TypeDefComposite, @@ -91,7 +89,7 @@ impl TypeSet { } } - fn collect_extrinsic_types(&mut self, extrinsic: &ExtrinsicMetadata) { + fn collect_extrinsic_types(&mut self, metadata: &Metadata, extrinsic: &ExtrinsicMetadata) { let mut ids = Vec::from([ extrinsic.address_ty, extrinsic.call_ty, @@ -104,17 +102,26 @@ impl TypeSet { ids.push(signed.additional_ty); } for id in ids { - self.seen_ids.insert(id); + if self.seen_ids.insert(id) { + let typ = resolve_typ(metadata, id); + self.insert_and_collect_types(metadata, typ); + } } } /// Collect all type IDs needed to represent the runtime APIs. - fn collect_runtime_api_types(&mut self, api: &RuntimeApiMetadataInner) { + fn collect_runtime_api_types(&mut self, metadata: &Metadata, api: &RuntimeApiMetadataInner) { for method in api.methods.values() { for input in &method.inputs { - self.seen_ids.insert(input.ty); + if self.seen_ids.insert(input.ty) { + let ty = resolve_typ(metadata, input.ty); + self.insert_and_collect_types(metadata, ty); + } + } + if self.seen_ids.insert(method.output_ty) { + let ty = resolve_typ(metadata, method.output_ty); + self.insert_and_collect_types(metadata, ty); } - self.seen_ids.insert(method.output_ty); } } @@ -392,7 +399,7 @@ pub fn retain_metadata( for api in metadata.apis.values() { let should_retain = runtime_apis_filter(&api.name); if should_retain { - type_set.collect_runtime_api_types(api); + type_set.collect_runtime_api_types(&metadata, api); } } @@ -406,7 +413,7 @@ pub fn retain_metadata( .expect("Metadata must contain sp_runtime::DispatchError"); type_set.seen_ids.insert(dispatch_error_ty.id); type_set.seen_ids.insert(metadata.runtime_ty); - type_set.collect_extrinsic_types(&metadata.extrinsic); + type_set.collect_extrinsic_types(&metadata, &metadata.extrinsic); // Collect the outer enums type IDs. for typ in [ diff --git a/metadata/src/utils/validation.rs b/metadata/src/utils/validation.rs index cea20328cf..c684c97131 100644 --- a/metadata/src/utils/validation.rs +++ b/metadata/src/utils/validation.rs @@ -122,15 +122,10 @@ fn get_type_def_variant_hash( let variant_field_bytes = variant.variants.iter().fold([0u8; HASH_LEN], |bytes, var| { // With EncodeAsType and DecodeAsType we no longer care which order the variants are in, // as long as all of the names+types are there. XOR to not care about ordering. - let should_hash = true; - if should_hash { - xor( - bytes, - get_variant_hash(registry, var, cache, outer_enum_hashes), - ) - } else { - bytes - } + xor( + bytes, + get_variant_hash(registry, var, cache, outer_enum_hashes), + ) }); concat_and_hash2(&variant_id_bytes, &variant_field_bytes) } From e5b1c82193afae795423bed94dad32becb5a9776 Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Wed, 2 Oct 2024 16:32:42 +0200 Subject: [PATCH 06/17] clippy --- metadata/src/utils/retain.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index ddbd619757..ed29f9d286 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -399,7 +399,7 @@ pub fn retain_metadata( for api in metadata.apis.values() { let should_retain = runtime_apis_filter(&api.name); if should_retain { - type_set.collect_runtime_api_types(&metadata, api); + type_set.collect_runtime_api_types(metadata, api); } } @@ -413,7 +413,7 @@ pub fn retain_metadata( .expect("Metadata must contain sp_runtime::DispatchError"); type_set.seen_ids.insert(dispatch_error_ty.id); type_set.seen_ids.insert(metadata.runtime_ty); - type_set.collect_extrinsic_types(&metadata, &metadata.extrinsic); + type_set.collect_extrinsic_types(metadata, &metadata.extrinsic); // Collect the outer enums type IDs. for typ in [ From da32fabd88daf34b6956f56c2346a3b8ff16bcfd Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Wed, 9 Oct 2024 09:01:34 +0200 Subject: [PATCH 07/17] review comments --- metadata/src/lib.rs | 5 +- metadata/src/utils/retain.rs | 167 +++++------------------------------ 2 files changed, 26 insertions(+), 146 deletions(-) diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index bce42b5b26..566c2fb1b9 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -214,7 +214,10 @@ impl Metadata { MetadataHasher::new(self) } - /// Filter out any pallets that we don't want to keep, retaining only those that we do. + /// Filter out any pallets and/or runtime_apis that we don't want to keep, retaining only those that we do. + /// Note: + /// without also filtering out irrelevant runtime_api's in addition to pallets one should not expect significant metadata size reduction. + /// This happens because we will keep the types returned by runtime_api methods to guarantee that we can safely decode them. pub fn retain(&mut self, pallet_filter: F, api_filter: G) where F: FnMut(&str) -> bool, diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index ed29f9d286..92790ae84c 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -15,8 +15,6 @@ use scale_info::{ TypeDefSequence, TypeDefTuple, TypeDefVariant, }; -use super::variant_index::VariantIndex; - #[derive(Clone)] struct TypeSet { seen_ids: BTreeSet, @@ -83,13 +81,14 @@ impl TypeSet { } } - fn insert_and_collect_types(&mut self, metadata: &Metadata, ty: &PortableType) { - if self.seen_ids.insert(ty.id) { - self.collect_types(metadata, ty); + fn insert_and_collect(&mut self, metadata: &Metadata, id: u32) { + if self.seen_ids.insert(id) { + let t = resolve_typ(metadata, id); + self.collect_types(metadata, t); } } - fn collect_extrinsic_types(&mut self, metadata: &Metadata, extrinsic: &ExtrinsicMetadata) { + fn collect_extrinsic_types(&mut self, extrinsic: &ExtrinsicMetadata) { let mut ids = Vec::from([ extrinsic.address_ty, extrinsic.call_ty, @@ -102,10 +101,7 @@ impl TypeSet { ids.push(signed.additional_ty); } for id in ids { - if self.seen_ids.insert(id) { - let typ = resolve_typ(metadata, id); - self.insert_and_collect_types(metadata, typ); - } + self.seen_ids.insert(id); } } @@ -113,15 +109,9 @@ impl TypeSet { fn collect_runtime_api_types(&mut self, metadata: &Metadata, api: &RuntimeApiMetadataInner) { for method in api.methods.values() { for input in &method.inputs { - if self.seen_ids.insert(input.ty) { - let ty = resolve_typ(metadata, input.ty); - self.insert_and_collect_types(metadata, ty); - } - } - if self.seen_ids.insert(method.output_ty) { - let ty = resolve_typ(metadata, method.output_ty); - self.insert_and_collect_types(metadata, ty); + self.insert_and_collect(metadata, input.ty); } + self.insert_and_collect(metadata, method.output_ty); } } @@ -160,73 +150,8 @@ impl TypeSet { type_ids.push(ty); } for id in type_ids { - let typ = resolve_typ(metadata, id); - self.insert_and_collect_types(metadata, typ); - } - } - /// Strips pallets that we need to keep around for their types - fn update_filtered_pallet( - &mut self, - pallet: &mut PalletMetadataInner, - retained_set: &mut TypeSet, - ) { - let entry_fn = |entry: &crate::StorageEntryMetadata| match entry.entry_type { - StorageEntryType::Plain(ty) => { - self.seen_ids.contains(&ty) && retained_set.seen_ids.remove(&ty) - } - StorageEntryType::Map { - key_ty, value_ty, .. - } => { - if self.seen_ids.contains(&key_ty) && self.seen_ids.contains(&value_ty) { - retained_set.seen_ids.remove(&key_ty) && retained_set.seen_ids.remove(&value_ty) - } else { - false - } - } - }; - let new_storage = match pallet.storage.as_mut() { - Some(storage) => { - // check if the both types in the seen_set and keep the entry if types were not retained already - storage.retain_entries(entry_fn); - // if the storage list is empty - drop it completetely - if storage.entries().is_empty() { - None - } else { - Some(storage) - } - } - None => None, - }; - - pallet.storage = new_storage.cloned(); - - // Helpers - let mut check_opt_and_retain = |option: Option| -> Option { - match option { - Some(ty) if self.seen_ids.contains(&ty) && retained_set.seen_ids.remove(&ty) => { - Some(ty) - } - _ => None, - } - }; - fn reset_variant_index(variant_index: &mut VariantIndex, opt: Option) { - if opt.is_none() { - *variant_index = VariantIndex::empty() - } + self.insert_and_collect(metadata, id); } - - pallet.call_ty = check_opt_and_retain(pallet.call_ty); - reset_variant_index(&mut pallet.call_variant_index, pallet.call_ty); - - pallet.event_ty = check_opt_and_retain(pallet.event_ty); - reset_variant_index(&mut pallet.event_variant_index, pallet.event_ty); - - pallet.error_ty = check_opt_and_retain(pallet.error_ty); - reset_variant_index(&mut pallet.error_variant_index, pallet.error_ty); - - pallet.constants.retain(|value| { - self.seen_ids.contains(&value.ty) && retained_set.seen_ids.remove(&value.ty) - }); } // Collect types referenced inside outer enum @@ -234,8 +159,6 @@ impl TypeSet { where F: FnMut(&str) -> bool, { - let m = metadata.clone(); - let ty = { metadata .types @@ -244,34 +167,15 @@ impl TypeSet { .expect("Metadata should contain enum type in registry") }; - let mut for_mut_ty = ty.clone(); - - let TypeDef::Variant(variant) = &mut for_mut_ty.ty.type_def else { - panic!("Metadata type is expected to be a variant type"); - }; - - // Remove all variants from the cloned type that aren't the pallet(s) we want to keep. - variant.variants.retain(|v| name_filter(&v.name)); - - // traverse the enum and collect the types - self.collect_types(&m, &for_mut_ty); - // Redo the thing above but keep filtered out variants if they reference types that we intend to keep let TypeDef::Variant(variant) = &mut ty.ty.type_def else { panic!("Metadata type is expected to be a variant type"); }; - variant.variants.retain(|v| { - name_filter(&v.name) || { - v.fields - .iter() - .all(|field| self.seen_ids.contains(&field.ty.id)) - } - }); - - self.seen_ids.insert(id); - - self.collect_types(&m, ty); + // If the type was not referenced earlier we can safely strip some of the variants + if self.seen_ids.insert(id) { + variant.variants.retain(|v| name_filter(&v.name)); + } } } @@ -395,7 +299,6 @@ pub fn retain_metadata( // all types that we intend to keep let mut type_set = retained_set.clone(); - for api in metadata.apis.values() { let should_retain = runtime_apis_filter(&api.name); if should_retain { @@ -413,7 +316,7 @@ pub fn retain_metadata( .expect("Metadata must contain sp_runtime::DispatchError"); type_set.seen_ids.insert(dispatch_error_ty.id); type_set.seen_ids.insert(metadata.runtime_ty); - type_set.collect_extrinsic_types(metadata, &metadata.extrinsic); + type_set.collect_extrinsic_types(&metadata.extrinsic); // Collect the outer enums type IDs. for typ in [ @@ -424,35 +327,10 @@ pub fn retain_metadata( type_set.collect_variants_in_type(metadata, typ, &mut pallets_filter); } - let mut retained_set = TypeSet { - seen_ids: type_set - .seen_ids - .difference(&retained_set.seen_ids) - .copied() - .collect(), - }; - // Strip down Pallets we dont need and only keep types that are not yet included in the retained set. - for pallet in metadata.pallets.values_mut() { - if !pallets_filter(&pallet.name) { - type_set.update_filtered_pallet(pallet, &mut retained_set); - } - } - // Filter out unnecesary pallets that have no entries - metadata.pallets.retain(|pallet| { - pallets_filter(&pallet.name) - || !matches!( - pallet, - PalletMetadataInner { - storage: None, - call_ty: None, - event_ty: None, - error_ty: None, - constants: map, - .. - } if map.is_empty() - ) - }); + metadata + .pallets + .retain(|pallet| pallets_filter(&pallet.name)); // Retain the apis metadata.apis.retain(|api| runtime_apis_filter(&api.name)); @@ -510,18 +388,17 @@ mod tests { |pallet_name| pallet_name == pallet.name(), |_| true, ); - - assert!( - metadata.pallets.len() < original_meta.pallets.len(), - "Stripped metadata must have less pallets than the non-stripped one: stripped amount {}, original amount {}", - metadata.pallets.len(), original_meta.pallets.len() + assert_eq!(metadata.pallets.len(), 1); + assert_eq!( + &*metadata.pallets.get_by_index(0).unwrap().name, + pallet.name() ); assert!( metadata.types.types.len() < original_meta.types.types.len(), "Stripped metadata must have less retained types than the non-stripped one: stripped amount {}, original amount {}", metadata.types.types.len(), original_meta.types.types.len() - ) + ); } } From cdfe63f0fa6034bb9b28172db3554b8e80806078 Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Mon, 14 Oct 2024 20:42:34 +0200 Subject: [PATCH 08/17] review comments closes #1659 --- artifacts/regressions/1659.scale | Bin 0 -> 60428 bytes metadata/src/lib.rs | 6 +-- metadata/src/utils/retain.rs | 66 +++++++++++++++++++++++-------- 3 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 artifacts/regressions/1659.scale diff --git a/artifacts/regressions/1659.scale b/artifacts/regressions/1659.scale new file mode 100644 index 0000000000000000000000000000000000000000..7f1975306496e2392ef0c5730fa3fb3c46fdacbd GIT binary patch literal 60428 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 literal 0 HcmV?d00001 diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 566c2fb1b9..da246e3246 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -216,8 +216,8 @@ impl Metadata { /// Filter out any pallets and/or runtime_apis that we don't want to keep, retaining only those that we do. /// Note: - /// without also filtering out irrelevant runtime_api's in addition to pallets one should not expect significant metadata size reduction. - /// This happens because we will keep the types returned by runtime_api methods to guarantee that we can safely decode them. + /// only filter by `pallet`s will not lead to significant metadata size reduction because the return types are kept to ensure that those can be decoded. + /// pub fn retain(&mut self, pallet_filter: F, api_filter: G) where F: FnMut(&str) -> bool, @@ -404,7 +404,7 @@ impl StorageMetadata { self.entries.values() } - /// filter over the storage entries. + /// Filter out storage entries that are not of interest. pub fn retain_entries(&mut self, f: F) where F: FnMut(&StorageEntryMetadata) -> bool, diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index 92790ae84c..cc8a959002 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -27,6 +27,7 @@ impl TypeSet { } } + /// This function will deeply traverse the inital type and it's dependencies to collect the relevant type_ids fn collect_types(&mut self, metadata: &Metadata, t: &PortableType) { let mut work_set = VecDeque::from([t]); while let Some(typ) = work_set.pop_front() { @@ -154,7 +155,8 @@ impl TypeSet { } } - // Collect types referenced inside outer enum + // Collect the types in outerEnums + // If the type wasn't previously collected we can safely strip some of the variants fn collect_variants_in_type(&mut self, metadata: &mut Metadata, id: u32, mut name_filter: F) where F: FnMut(&str) -> bool, @@ -167,7 +169,6 @@ impl TypeSet { .expect("Metadata should contain enum type in registry") }; - // Redo the thing above but keep filtered out variants if they reference types that we intend to keep let TypeDef::Variant(variant) = &mut ty.ty.type_def else { panic!("Metadata type is expected to be a variant type"); }; @@ -287,18 +288,18 @@ pub fn retain_metadata( F: FnMut(&str) -> bool, G: FnMut(&str) -> bool, { - // Types specifically referenced inside pallets that we keep - let mut retained_set = TypeSet::new(); + // all types that we intend to keep + let mut type_set = TypeSet::new(); + // Do a deep traversal over the pallet types first for pallet in metadata.pallets.values() { let should_retain = pallets_filter(&pallet.name); if should_retain { - retained_set.collect_pallet_types(pallet, metadata); + type_set.collect_pallet_types(pallet, metadata); } } - // all types that we intend to keep - let mut type_set = retained_set.clone(); + // Do a deep traversal over the `Runtime apis` input/output types for api in metadata.apis.values() { let should_retain = runtime_apis_filter(&api.name); if should_retain { @@ -327,14 +328,14 @@ pub fn retain_metadata( type_set.collect_variants_in_type(metadata, typ, &mut pallets_filter); } + // Retain the apis + metadata.apis.retain(|api| runtime_apis_filter(&api.name)); + // Filter out unnecesary pallets that have no entries + // and then re-index pallets after removing some of them above metadata .pallets .retain(|pallet| pallets_filter(&pallet.name)); - - // Retain the apis - metadata.apis.retain(|api| runtime_apis_filter(&api.name)); - metadata.pallets_by_index = metadata .pallets .values() @@ -362,9 +363,12 @@ mod tests { use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed}; use std::{fs, path::Path}; - fn load_metadata() -> Metadata { - let bytes = fs::read(Path::new("../artifacts/polkadot_metadata_full.scale")) - .expect("Cannot read metadata blob"); + fn load_metadata_polkadot() -> Metadata { + load_metadata("../artifacts/polkadot_metadata_full.scale") + } + + fn load_metadata(path: impl AsRef) -> Metadata { + let bytes = fs::read(path).expect("Cannot read metadata blob"); let meta: RuntimeMetadataPrefixed = Decode::decode(&mut &*bytes).expect("Cannot decode scale metadata"); @@ -377,7 +381,7 @@ mod tests { #[test] fn retain_one_pallet() { - let metadata_cache = load_metadata(); + let metadata_cache = load_metadata_polkadot(); // Retain one pallet at a time ensuring the test does not panic. for pallet in metadata_cache.pallets() { @@ -404,7 +408,7 @@ mod tests { #[test] fn retain_one_runtime_api() { - let metadata_cache = load_metadata(); + let metadata_cache = load_metadata_polkadot(); // Retain one runtime API at a time ensuring the test does not panic. for runtime_api in metadata_cache.runtime_api_traits() { @@ -422,4 +426,34 @@ mod tests { ); } } + + #[test] + fn issue_1659() { + let metadata_cache = load_metadata("../artifacts/regressions/1659.scale"); + + // Strip metadata to the pallets as described in the issue. + let mut stripped_metadata = metadata_cache.clone(); + retain_metadata( + &mut stripped_metadata, + { + let set = "Balances,Timestamp,Contracts,ContractsEvm,System" + .split(",") + .collect::>(); + move |s| set.contains(&s) + }, + |_| true, + ); + + // check that call_enum did not change as it is referenced inside runtime_api + assert_eq!( + stripped_metadata.type_hash(stripped_metadata.outer_enums.call_enum_ty), + metadata_cache.type_hash(metadata_cache.outer_enums.call_enum_ty) + ); + + // check that event_num did not change as it is referenced inside runtime_api + assert_eq!( + stripped_metadata.type_hash(stripped_metadata.outer_enums.event_enum_ty), + metadata_cache.type_hash(metadata_cache.outer_enums.event_enum_ty) + ); + } } From 7e0cb699b53f4158e30bed4d358e13d244c294b8 Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Mon, 14 Oct 2024 20:50:41 +0200 Subject: [PATCH 09/17] review comments --- metadata/src/utils/retain.rs | 60 +++++++++++++++++--------------- metadata/src/utils/validation.rs | 2 +- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index cc8a959002..6a86a667ac 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -9,7 +9,6 @@ use crate::{ StorageEntryType, }; use alloc::collections::{BTreeMap, BTreeSet, VecDeque}; -use alloc::vec::Vec; use scale_info::{ PortableType, TypeDef, TypeDefArray, TypeDefBitSequence, TypeDefCompact, TypeDefComposite, TypeDefSequence, TypeDefTuple, TypeDefVariant, @@ -27,6 +26,14 @@ impl TypeSet { } } + fn insert(&mut self, id: u32) -> bool { + self.seen_ids.insert(id) + } + + fn contains(&mut self, id: u32) -> bool { + self.seen_ids.contains(&id) + } + /// This function will deeply traverse the inital type and it's dependencies to collect the relevant type_ids fn collect_types(&mut self, metadata: &Metadata, t: &PortableType) { let mut work_set = VecDeque::from([t]); @@ -34,7 +41,7 @@ impl TypeSet { match &typ.ty.type_def { TypeDef::Composite(TypeDefComposite { fields }) => { for field in fields { - if self.seen_ids.insert(field.ty.id) { + if self.insert(field.ty.id) { let ty = resolve_typ(metadata, field.ty.id); work_set.push_back(ty); } @@ -43,7 +50,7 @@ impl TypeSet { TypeDef::Variant(TypeDefVariant { variants }) => { for variant in variants { for field in &variant.fields { - if self.seen_ids.insert(field.ty.id) { + if self.insert(field.ty.id) { let ty = resolve_typ(metadata, field.ty.id); work_set.push_back(ty); } @@ -53,14 +60,14 @@ impl TypeSet { TypeDef::Array(TypeDefArray { len: _, type_param }) | TypeDef::Sequence(TypeDefSequence { type_param }) | TypeDef::Compact(TypeDefCompact { type_param }) => { - if self.seen_ids.insert(type_param.id) { + if self.insert(type_param.id) { let ty = resolve_typ(metadata, type_param.id); work_set.push_back(ty); } } TypeDef::Tuple(TypeDefTuple { fields }) => { for field in fields { - if self.seen_ids.insert(field.id) { + if self.insert(field.id) { let ty = resolve_typ(metadata, field.id); work_set.push_back(ty); } @@ -72,7 +79,7 @@ impl TypeSet { bit_order_type, }) => { for typ in [bit_order_type, bit_store_type] { - if self.seen_ids.insert(typ.id) { + if self.insert(typ.id) { let ty = resolve_typ(metadata, typ.id); work_set.push_back(ty); } @@ -83,26 +90,25 @@ impl TypeSet { } fn insert_and_collect(&mut self, metadata: &Metadata, id: u32) { - if self.seen_ids.insert(id) { + if self.insert(id) { let t = resolve_typ(metadata, id); self.collect_types(metadata, t); } } fn collect_extrinsic_types(&mut self, extrinsic: &ExtrinsicMetadata) { - let mut ids = Vec::from([ + for ty in [ extrinsic.address_ty, extrinsic.call_ty, extrinsic.signature_ty, extrinsic.extra_ty, - ]); + ] { + self.insert(ty); + } for signed in &extrinsic.signed_extensions { - ids.push(signed.extra_ty); - ids.push(signed.additional_ty); - } - for id in ids { - self.seen_ids.insert(id); + self.insert(signed.extra_ty); + self.insert(signed.additional_ty); } } @@ -118,40 +124,36 @@ impl TypeSet { /// Collect all type IDs needed to represent the provided pallet. fn collect_pallet_types(&mut self, pallet: &PalletMetadataInner, metadata: &Metadata) { - let mut type_ids = Vec::new(); if let Some(storage) = &pallet.storage { for entry in storage.entries() { match entry.entry_type { StorageEntryType::Plain(ty) => { - type_ids.push(ty); + self.insert_and_collect(metadata, ty); } StorageEntryType::Map { key_ty, value_ty, .. } => { - type_ids.push(key_ty); - type_ids.push(value_ty); + self.insert_and_collect(metadata, key_ty); + self.insert_and_collect(metadata, value_ty); } } } } if let Some(ty) = pallet.call_ty { - type_ids.push(ty); + self.insert_and_collect(metadata, ty); } if let Some(ty) = pallet.event_ty { - type_ids.push(ty); + self.insert_and_collect(metadata, ty); } for constant in pallet.constants.values() { - type_ids.push(constant.ty); + self.insert_and_collect(metadata, constant.ty); } if let Some(ty) = pallet.error_ty { - type_ids.push(ty); - } - for id in type_ids { - self.insert_and_collect(metadata, id); + self.insert_and_collect(metadata, ty); } } @@ -174,7 +176,7 @@ impl TypeSet { }; // If the type was not referenced earlier we can safely strip some of the variants - if self.seen_ids.insert(id) { + if self.insert(id) { variant.variants.retain(|v| name_filter(&v.name)); } } @@ -315,8 +317,8 @@ pub fn retain_metadata( .iter() .find(|ty| ty.ty.path.segments == ["sp_runtime", "DispatchError"]) .expect("Metadata must contain sp_runtime::DispatchError"); - type_set.seen_ids.insert(dispatch_error_ty.id); - type_set.seen_ids.insert(metadata.runtime_ty); + type_set.insert(dispatch_error_ty.id); + type_set.insert(metadata.runtime_ty); type_set.collect_extrinsic_types(&metadata.extrinsic); // Collect the outer enums type IDs. @@ -344,7 +346,7 @@ pub fn retain_metadata( .map(|(pos, p)| (p.index, pos)) .collect(); - let map_ids = metadata.types.retain(|id| type_set.seen_ids.contains(&id)); + let map_ids = metadata.types.retain(|id| type_set.contains(id)); update_outer_enums(&mut metadata.outer_enums, &map_ids); for pallets in metadata.pallets.values_mut() { diff --git a/metadata/src/utils/validation.rs b/metadata/src/utils/validation.rs index c684c97131..8d7cef58b1 100644 --- a/metadata/src/utils/validation.rs +++ b/metadata/src/utils/validation.rs @@ -577,7 +577,7 @@ impl<'a> MetadataHasher<'a> { pub fn hash(&self) -> Hash { let metadata = self.metadata; - // Get the hashes of outer enums, considering only `specific_pallets` (if any are set). + // Get the hashes of outer enums. // If any of the typed that represent outer enums are encountered later, hashes from `top_level_enum_hashes` can be substituted. let outer_enum_hashes = OuterEnumHashes::new(metadata); From 7e063dc7f4882a9eb1facea6a2b72750fa1cd9c2 Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Tue, 15 Oct 2024 11:47:17 +0200 Subject: [PATCH 10/17] review comments --- metadata/src/utils/retain.rs | 70 ++++++++++++-------------------- metadata/src/utils/validation.rs | 25 ------------ 2 files changed, 26 insertions(+), 69 deletions(-) diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index 6a86a667ac..59959c7a89 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -35,42 +35,34 @@ impl TypeSet { } /// This function will deeply traverse the inital type and it's dependencies to collect the relevant type_ids - fn collect_types(&mut self, metadata: &Metadata, t: &PortableType) { - let mut work_set = VecDeque::from([t]); + fn collect_types(&mut self, metadata: &Metadata, id: u32) { + let mut work_set = VecDeque::from([id]); while let Some(typ) = work_set.pop_front() { + if !self.insert(typ) { + return; + } + let typ = resolve_typ(metadata, typ); match &typ.ty.type_def { TypeDef::Composite(TypeDefComposite { fields }) => { for field in fields { - if self.insert(field.ty.id) { - let ty = resolve_typ(metadata, field.ty.id); - work_set.push_back(ty); - } + work_set.push_back(field.ty.id); } } TypeDef::Variant(TypeDefVariant { variants }) => { for variant in variants { for field in &variant.fields { - if self.insert(field.ty.id) { - let ty = resolve_typ(metadata, field.ty.id); - work_set.push_back(ty); - } + work_set.push_back(field.ty.id); } } } TypeDef::Array(TypeDefArray { len: _, type_param }) | TypeDef::Sequence(TypeDefSequence { type_param }) | TypeDef::Compact(TypeDefCompact { type_param }) => { - if self.insert(type_param.id) { - let ty = resolve_typ(metadata, type_param.id); - work_set.push_back(ty); - } + work_set.push_back(type_param.id); } TypeDef::Tuple(TypeDefTuple { fields }) => { for field in fields { - if self.insert(field.id) { - let ty = resolve_typ(metadata, field.id); - work_set.push_back(ty); - } + work_set.push_back(field.id); } } TypeDef::Primitive(_) => (), @@ -79,23 +71,13 @@ impl TypeSet { bit_order_type, }) => { for typ in [bit_order_type, bit_store_type] { - if self.insert(typ.id) { - let ty = resolve_typ(metadata, typ.id); - work_set.push_back(ty); - } + work_set.push_back(typ.id); } } } } } - fn insert_and_collect(&mut self, metadata: &Metadata, id: u32) { - if self.insert(id) { - let t = resolve_typ(metadata, id); - self.collect_types(metadata, t); - } - } - fn collect_extrinsic_types(&mut self, extrinsic: &ExtrinsicMetadata) { for ty in [ extrinsic.address_ty, @@ -116,9 +98,9 @@ impl TypeSet { fn collect_runtime_api_types(&mut self, metadata: &Metadata, api: &RuntimeApiMetadataInner) { for method in api.methods.values() { for input in &method.inputs { - self.insert_and_collect(metadata, input.ty); + self.collect_types(metadata, input.ty); } - self.insert_and_collect(metadata, method.output_ty); + self.collect_types(metadata, method.output_ty); } } @@ -128,32 +110,32 @@ impl TypeSet { for entry in storage.entries() { match entry.entry_type { StorageEntryType::Plain(ty) => { - self.insert_and_collect(metadata, ty); + self.collect_types(metadata, ty); } StorageEntryType::Map { key_ty, value_ty, .. } => { - self.insert_and_collect(metadata, key_ty); - self.insert_and_collect(metadata, value_ty); + self.collect_types(metadata, key_ty); + self.collect_types(metadata, value_ty); } } } } if let Some(ty) = pallet.call_ty { - self.insert_and_collect(metadata, ty); + self.collect_types(metadata, ty); } if let Some(ty) = pallet.event_ty { - self.insert_and_collect(metadata, ty); + self.collect_types(metadata, ty); } for constant in pallet.constants.values() { - self.insert_and_collect(metadata, constant.ty); + self.collect_types(metadata, constant.ty); } if let Some(ty) = pallet.error_ty { - self.insert_and_collect(metadata, ty); + self.collect_types(metadata, ty); } } @@ -365,11 +347,11 @@ mod tests { use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed}; use std::{fs, path::Path}; - fn load_metadata_polkadot() -> Metadata { - load_metadata("../artifacts/polkadot_metadata_full.scale") + fn load_metadata() -> Metadata { + load_metadata_custom("../artifacts/polkadot_metadata_full.scale") } - fn load_metadata(path: impl AsRef) -> Metadata { + fn load_metadata_custom(path: impl AsRef) -> Metadata { let bytes = fs::read(path).expect("Cannot read metadata blob"); let meta: RuntimeMetadataPrefixed = Decode::decode(&mut &*bytes).expect("Cannot decode scale metadata"); @@ -383,7 +365,7 @@ mod tests { #[test] fn retain_one_pallet() { - let metadata_cache = load_metadata_polkadot(); + let metadata_cache = load_metadata(); // Retain one pallet at a time ensuring the test does not panic. for pallet in metadata_cache.pallets() { @@ -410,7 +392,7 @@ mod tests { #[test] fn retain_one_runtime_api() { - let metadata_cache = load_metadata_polkadot(); + let metadata_cache = load_metadata(); // Retain one runtime API at a time ensuring the test does not panic. for runtime_api in metadata_cache.runtime_api_traits() { @@ -431,7 +413,7 @@ mod tests { #[test] fn issue_1659() { - let metadata_cache = load_metadata("../artifacts/regressions/1659.scale"); + let metadata_cache = load_metadata_custom("../artifacts/regressions/1659.scale"); // Strip metadata to the pallets as described in the issue. let mut stripped_metadata = metadata_cache.clone(); diff --git a/metadata/src/utils/validation.rs b/metadata/src/utils/validation.rs index eb45dd41ca..28ccd187c1 100644 --- a/metadata/src/utils/validation.rs +++ b/metadata/src/utils/validation.rs @@ -1216,29 +1216,4 @@ mod tests { .try_into() .expect("can build valid metadata") } - - #[test] - fn hash_comparison_trimmed_metadata() { - // trim the metadata: - let metadata = metadata_with_pallet_events(); - let trimmed_metadata = { - let mut m = metadata.clone(); - m.retain(|e| e == "First", |_| true); - m - }; - - let name_set: Vec = trimmed_metadata - .pallets - .values() - .iter() - .map(|pallet| pallet.name.to_string()) - .collect(); - // test that the hashes are the same: - let hash = MetadataHasher::new(&metadata) - .only_these_pallets(&name_set) - .hash(); - let hash_trimmed = MetadataHasher::new(&trimmed_metadata).hash(); - - assert_eq!(hash, hash_trimmed); - } } From 292bec480e61665ef0525055845a5545f9930eff Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Tue, 15 Oct 2024 12:44:23 +0200 Subject: [PATCH 11/17] review comment --- metadata/src/lib.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index da246e3246..1d81efe200 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -404,14 +404,6 @@ impl StorageMetadata { self.entries.values() } - /// Filter out storage entries that are not of interest. - pub fn retain_entries(&mut self, f: F) - where - F: FnMut(&StorageEntryMetadata) -> bool, - { - self.entries.retain(f) - } - /// Return a specific storage entry given its name. pub fn entry_by_name(&self, name: &str) -> Option<&StorageEntryMetadata> { self.entries.get_by_key(name) From 4275bc517e561a4e5be497bfcd826bc6b26b3aa8 Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Tue, 15 Oct 2024 15:27:43 +0200 Subject: [PATCH 12/17] clippy --- metadata/src/utils/validation.rs | 121 ------------------------------- 1 file changed, 121 deletions(-) diff --git a/metadata/src/utils/validation.rs b/metadata/src/utils/validation.rs index 28ccd187c1..77b072d1e5 100644 --- a/metadata/src/utils/validation.rs +++ b/metadata/src/utils/validation.rs @@ -1095,125 +1095,4 @@ mod tests { to_hash(meta_type::()) ); } - - use frame_metadata::v15::{ - PalletEventMetadata, PalletStorageMetadata, StorageEntryMetadata, StorageEntryModifier, - }; - - fn metadata_with_pallet_events() -> Metadata { - #[allow(dead_code)] - #[derive(scale_info::TypeInfo)] - struct FirstEvent { - s: String, - } - - #[allow(dead_code)] - #[derive(scale_info::TypeInfo)] - struct SecondEvent { - n: u8, - } - - #[allow(dead_code)] - #[derive(scale_info::TypeInfo)] - enum Events { - First(FirstEvent), - Second(SecondEvent), - } - - #[allow(dead_code)] - #[derive(scale_info::TypeInfo)] - enum Errors { - First(DispatchError), - Second(DispatchError), - } - - #[allow(dead_code)] - #[derive(scale_info::TypeInfo)] - enum Calls { - First(u8), - Second(u8), - } - - #[allow(dead_code)] - enum DispatchError { - A, - B, - C, - } - - impl scale_info::TypeInfo for DispatchError { - type Identity = DispatchError; - - fn type_info() -> scale_info::Type { - scale_info::Type { - path: scale_info::Path { - segments: vec!["sp_runtime", "DispatchError"], - }, - type_params: vec![], - type_def: TypeDef::Variant(TypeDefVariant { variants: vec![] }), - docs: vec![], - } - } - } - - let pallets = vec![ - v15::PalletMetadata { - name: "First", - index: 0, - calls: Some(v15::PalletCallMetadata { - ty: meta_type::(), - }), - storage: Some(PalletStorageMetadata { - prefix: "___", - entries: vec![StorageEntryMetadata { - name: "Hello", - modifier: StorageEntryModifier::Optional, - // Note: This is the important part here: - // The Events type will be trimmed down and this trimming needs to be reflected - // when the hash of this storage item is computed. - ty: frame_metadata::v14::StorageEntryType::Plain(meta_type::>()), - default: vec![], - docs: vec![], - }], - }), - event: Some(PalletEventMetadata { - ty: meta_type::(), - }), - constants: vec![], - error: None, - docs: vec![], - }, - v15::PalletMetadata { - name: "Second", - index: 1, - calls: Some(v15::PalletCallMetadata { - ty: meta_type::(), - }), - storage: None, - event: Some(PalletEventMetadata { - ty: meta_type::(), - }), - constants: vec![], - error: None, - docs: vec![], - }, - ]; - - v15::RuntimeMetadataV15::new( - pallets, - build_default_extrinsic(), - meta_type::<()>(), - vec![], - v15::OuterEnums { - call_enum_ty: meta_type::(), - event_enum_ty: meta_type::(), - error_enum_ty: meta_type::(), - }, - v15::CustomMetadata { - map: Default::default(), - }, - ) - .try_into() - .expect("can build valid metadata") - } } From c5d1787ef624705c41c0cd28c224e1d365eb3659 Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Thu, 17 Oct 2024 16:16:35 +0200 Subject: [PATCH 13/17] review comments --- metadata/src/utils/retain.rs | 408 +++++++++--------- metadata/src/utils/validation.rs | 167 ++++++- .../src/utils/validation/outer_enum_hashes.rs | 46 +- 3 files changed, 401 insertions(+), 220 deletions(-) diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index 59959c7a89..877d4f9341 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -5,10 +5,9 @@ //! Utility functions to generate a subset of the metadata. use crate::{ - ExtrinsicMetadata, Metadata, OuterEnumsMetadata, PalletMetadataInner, RuntimeApiMetadataInner, - StorageEntryType, + ExtrinsicMetadata, Metadata, PalletMetadataInner, RuntimeApiMetadataInner, StorageEntryType, }; -use alloc::collections::{BTreeMap, BTreeSet, VecDeque}; +use alloc::collections::{BTreeSet, VecDeque}; use scale_info::{ PortableType, TypeDef, TypeDefArray, TypeDefBitSequence, TypeDefCompact, TypeDefComposite, TypeDefSequence, TypeDefTuple, TypeDefVariant, @@ -17,12 +16,14 @@ use scale_info::{ #[derive(Clone)] struct TypeSet { seen_ids: BTreeSet, + work_set: VecDeque, } impl TypeSet { fn new() -> Self { Self { seen_ids: BTreeSet::new(), + work_set: VecDeque::with_capacity(22), } } @@ -36,42 +37,41 @@ impl TypeSet { /// This function will deeply traverse the inital type and it's dependencies to collect the relevant type_ids fn collect_types(&mut self, metadata: &Metadata, id: u32) { - let mut work_set = VecDeque::from([id]); - while let Some(typ) = work_set.pop_front() { - if !self.insert(typ) { - return; - } - let typ = resolve_typ(metadata, typ); - match &typ.ty.type_def { - TypeDef::Composite(TypeDefComposite { fields }) => { - for field in fields { - work_set.push_back(field.ty.id); + self.work_set.push_back(id); + while let Some(typ) = self.work_set.pop_front() { + if self.insert(typ) { + let typ = resolve_typ(metadata, typ); + match &typ.ty.type_def { + TypeDef::Composite(TypeDefComposite { fields }) => { + for field in fields { + self.work_set.push_back(field.ty.id); + } } - } - TypeDef::Variant(TypeDefVariant { variants }) => { - for variant in variants { - for field in &variant.fields { - work_set.push_back(field.ty.id); + TypeDef::Variant(TypeDefVariant { variants }) => { + for variant in variants { + for field in &variant.fields { + self.work_set.push_back(field.ty.id); + } } } - } - TypeDef::Array(TypeDefArray { len: _, type_param }) - | TypeDef::Sequence(TypeDefSequence { type_param }) - | TypeDef::Compact(TypeDefCompact { type_param }) => { - work_set.push_back(type_param.id); - } - TypeDef::Tuple(TypeDefTuple { fields }) => { - for field in fields { - work_set.push_back(field.id); + TypeDef::Array(TypeDefArray { len: _, type_param }) + | TypeDef::Sequence(TypeDefSequence { type_param }) + | TypeDef::Compact(TypeDefCompact { type_param }) => { + self.work_set.push_back(type_param.id); } - } - TypeDef::Primitive(_) => (), - TypeDef::BitSequence(TypeDefBitSequence { - bit_store_type, - bit_order_type, - }) => { - for typ in [bit_order_type, bit_store_type] { - work_set.push_back(typ.id); + TypeDef::Tuple(TypeDefTuple { fields }) => { + for field in fields { + self.work_set.push_back(field.id); + } + } + TypeDef::Primitive(_) => (), + TypeDef::BitSequence(TypeDefBitSequence { + bit_store_type, + bit_order_type, + }) => { + for typ in [bit_order_type, bit_store_type] { + self.work_set.push_back(typ.id); + } } } } @@ -97,9 +97,6 @@ impl TypeSet { /// Collect all type IDs needed to represent the runtime APIs. fn collect_runtime_api_types(&mut self, metadata: &Metadata, api: &RuntimeApiMetadataInner) { for method in api.methods.values() { - for input in &method.inputs { - self.collect_types(metadata, input.ty); - } self.collect_types(metadata, method.output_ty); } } @@ -122,45 +119,9 @@ impl TypeSet { } } - if let Some(ty) = pallet.call_ty { - self.collect_types(metadata, ty); - } - - if let Some(ty) = pallet.event_ty { - self.collect_types(metadata, ty); - } - for constant in pallet.constants.values() { self.collect_types(metadata, constant.ty); } - - if let Some(ty) = pallet.error_ty { - self.collect_types(metadata, ty); - } - } - - // Collect the types in outerEnums - // If the type wasn't previously collected we can safely strip some of the variants - fn collect_variants_in_type(&mut self, metadata: &mut Metadata, id: u32, mut name_filter: F) - where - F: FnMut(&str) -> bool, - { - let ty = { - metadata - .types - .types - .get_mut(id as usize) - .expect("Metadata should contain enum type in registry") - }; - - let TypeDef::Variant(variant) = &mut ty.ty.type_def else { - panic!("Metadata type is expected to be a variant type"); - }; - - // If the type was not referenced earlier we can safely strip some of the variants - if self.insert(id) { - variant.variants.retain(|v| name_filter(&v.name)); - } } } @@ -172,86 +133,6 @@ fn resolve_typ(metadata: &Metadata, typ: u32) -> &PortableType { .expect("Metadata should contain enum type in registry") } -/// Update all type IDs of the provided pallet using the new type IDs from the portable registry. -fn update_pallet_types(pallet: &mut PalletMetadataInner, map_ids: &BTreeMap) { - if let Some(storage) = &mut pallet.storage { - for entry in storage.entries.values_mut() { - match &mut entry.entry_type { - StorageEntryType::Plain(ty) => { - update_type(ty, map_ids); - } - StorageEntryType::Map { - key_ty, value_ty, .. - } => { - update_type(key_ty, map_ids); - update_type(value_ty, map_ids); - } - } - } - } - if let Some(ty) = &mut pallet.call_ty { - update_type(ty, map_ids); - } - - if let Some(ty) = &mut pallet.event_ty { - update_type(ty, map_ids); - } - - if let Some(ty) = &mut pallet.error_ty { - update_type(ty, map_ids); - } - - for constant in pallet.constants.values_mut() { - update_type(&mut constant.ty, map_ids); - } -} - -/// Update all type IDs of the provided extrinsic metadata using the new type IDs from the portable registry. -fn update_extrinsic_types(extrinsic: &mut ExtrinsicMetadata, map_ids: &BTreeMap) { - update_type(&mut extrinsic.address_ty, map_ids); - update_type(&mut extrinsic.call_ty, map_ids); - update_type(&mut extrinsic.signature_ty, map_ids); - update_type(&mut extrinsic.extra_ty, map_ids); - - for signed in &mut extrinsic.signed_extensions { - update_type(&mut signed.extra_ty, map_ids); - update_type(&mut signed.additional_ty, map_ids); - } -} - -/// Update all type IDs of the provided runtime APIs metadata using the new type IDs from the portable registry. -fn update_runtime_api_types(apis: &mut [RuntimeApiMetadataInner], map_ids: &BTreeMap) { - for api in apis { - for method in api.methods.values_mut() { - for input in &mut method.inputs { - update_type(&mut input.ty, map_ids); - } - update_type(&mut method.output_ty, map_ids); - } - } -} - -/// Update all the type IDs for outer enums. -fn update_outer_enums(enums: &mut OuterEnumsMetadata, map_ids: &BTreeMap) { - update_type(&mut enums.call_enum_ty, map_ids); - update_type(&mut enums.event_enum_ty, map_ids); - update_type(&mut enums.error_enum_ty, map_ids); -} - -/// Update the given type using the new type ID from the portable registry. -/// -/// # Panics -/// -/// Panics if the [`scale_info::PortableRegistry`] did not retain all needed types. -fn update_type(ty: &mut u32, map_ids: &BTreeMap) { - let old_id = *ty; - let new_id = map_ids - .get(&old_id) - .copied() - .unwrap_or_else(|| panic!("PortableRegistry did not retain type id {old_id}. This is a bug. Please open an issue.")); - *ty = new_id; -} - /// Generate a subset of the metadata that contains only the /// types needed to represent the provided pallets and runtime APIs. /// @@ -272,77 +153,193 @@ pub fn retain_metadata( F: FnMut(&str) -> bool, G: FnMut(&str) -> bool, { - // all types that we intend to keep - let mut type_set = TypeSet::new(); + // 1. Delete pallets we don't want to keep. + metadata + .pallets + .retain(|pallet| pallets_filter(&pallet.name)); + metadata.pallets_by_index = metadata + .pallets + .values() + .iter() + .enumerate() + .map(|(pos, p)| (p.index, pos)) + .collect(); - // Do a deep traversal over the pallet types first - for pallet in metadata.pallets.values() { - let should_retain = pallets_filter(&pallet.name); - if should_retain { - type_set.collect_pallet_types(pallet, metadata); - } - } + // 2. Delete runtime APIs we don't want to keep. + metadata.apis.retain(|api| runtime_apis_filter(&api.name)); - // Do a deep traversal over the `Runtime apis` input/output types - for api in metadata.apis.values() { - let should_retain = runtime_apis_filter(&api.name); - if should_retain { - type_set.collect_runtime_api_types(metadata, api); + // 3. For each outer enum type, strip it if possible, ie if it is not returned by any + // of the things we're keeping (because if it is, we need to keep all of it so that we + // can still decode values into it). + let outer_enums = metadata.outer_enums(); + let mut find_type_id = + collect_return_types(metadata, &mut pallets_filter, &mut runtime_apis_filter); + for outer_enum_ty_id in [ + outer_enums.call_enum_ty(), + outer_enums.error_enum_ty(), + outer_enums.event_enum_ty(), + ] { + if !find_type_id(outer_enum_ty_id) { + strip_variants_in_enum_type(metadata, &mut pallets_filter, outer_enum_ty_id); } } - // Additionally, subxt depends on the `DispatchError` type existing; we use the same - // logic here that is used when building our `Metadata`. + // 4. Collect all of the type IDs we still want to keep after deleting. + let mut keep_these_type_ids: BTreeSet = iterate_metadata_types(metadata) + .iter() + .map(|x| **x) + .collect(); + + // 5. Additionally, subxt depends on the `DispatchError` type existing; we use the same + // logic here that is used when building our `Metadata` to ensure we keep it too. let dispatch_error_ty = metadata .types .types .iter() .find(|ty| ty.ty.path.segments == ["sp_runtime", "DispatchError"]) .expect("Metadata must contain sp_runtime::DispatchError"); - type_set.insert(dispatch_error_ty.id); - type_set.insert(metadata.runtime_ty); - type_set.collect_extrinsic_types(&metadata.extrinsic); - // Collect the outer enums type IDs. - for typ in [ - metadata.outer_enums.call_enum_ty, - metadata.outer_enums.event_enum_ty, - metadata.outer_enums.error_enum_ty, + keep_these_type_ids.insert(dispatch_error_ty.id); + + // 5. Strip all of the type IDs we no longer need, based on the above set. + let map_ids = metadata + .types + .retain(|id| keep_these_type_ids.contains(&id)); + + // 6. Now, update the type IDs referenced in our metadata to reflect this. + for id in iterate_metadata_types(metadata) { + if let Some(new_id) = map_ids.get(id) { + *id = *new_id; + } else { + panic!("Type id {id} was not retained. This is a bug"); + } + } +} + +fn strip_variants_in_enum_type(metadata: &mut Metadata, mut pallets_filter: F, id: u32) +where + F: FnMut(&str) -> bool, +{ + let ty = { + metadata + .types + .types + .get_mut(id as usize) + .expect("Metadata should contain enum type in registry") + }; + + let TypeDef::Variant(variant) = &mut ty.ty.type_def else { + panic!("Metadata type is expected to be a variant type"); + }; + + variant.variants.retain(|v| pallets_filter(&v.name)); +} + +/// return an Vec handing back a mutable reference to each type ID seen in the metadata (not recursively). +/// This will iterate over every type referenced in the metadata outside of `metadata.types`. +fn iterate_metadata_types(metadata: &mut Metadata) -> alloc::vec::Vec<&mut u32> { + let mut types = alloc::vec::Vec::new(); + + // collect outer_enum top-level types + let outer_enum = &mut metadata.outer_enums; + types.push(&mut outer_enum.call_enum_ty); + types.push(&mut outer_enum.event_enum_ty); + types.push(&mut outer_enum.error_enum_ty); + + // collect pallet top-level type ids + for pallet in metadata.pallets.values_mut() { + if let Some(storage) = &mut pallet.storage { + for entry in storage.entries.values_mut() { + match &mut entry.entry_type { + StorageEntryType::Plain(ty) => { + types.push(ty); + } + StorageEntryType::Map { + key_ty, value_ty, .. + } => { + types.push(key_ty); + types.push(value_ty); + } + } + } + }; + if let Some(ty) = &mut pallet.call_ty { + types.push(ty); + } + + if let Some(ty) = &mut pallet.event_ty { + types.push(ty); + } + + if let Some(ty) = &mut pallet.error_ty { + types.push(ty); + } + + for constant in pallet.constants.values_mut() { + types.push(&mut constant.ty); + } + } + + // collect extrinsic type_ids + for ty in [ + &mut metadata.extrinsic.extra_ty, + &mut metadata.extrinsic.address_ty, + &mut metadata.extrinsic.signature_ty, + &mut metadata.extrinsic.call_ty, ] { - type_set.collect_variants_in_type(metadata, typ, &mut pallets_filter); + types.push(ty); } - // Retain the apis - metadata.apis.retain(|api| runtime_apis_filter(&api.name)); + for signed in &mut metadata.extrinsic.signed_extensions { + types.push(&mut signed.extra_ty); + types.push(&mut signed.additional_ty); + } - // Filter out unnecesary pallets that have no entries - // and then re-index pallets after removing some of them above - metadata - .pallets - .retain(|pallet| pallets_filter(&pallet.name)); - metadata.pallets_by_index = metadata - .pallets - .values() - .iter() - .enumerate() - .map(|(pos, p)| (p.index, pos)) - .collect(); + types.push(&mut metadata.runtime_ty); + + // collect runtime_api_types + for api in metadata.apis.values_mut() { + for method in api.methods.values_mut() { + for input in &mut method.inputs.iter_mut() { + types.push(&mut input.ty); + } + types.push(&mut method.output_ty); + } + } - let map_ids = metadata.types.retain(|id| type_set.contains(id)); + types +} - update_outer_enums(&mut metadata.outer_enums, &map_ids); - for pallets in metadata.pallets.values_mut() { - update_pallet_types(pallets, &map_ids); +/// Look for a type ID anywhere that we can be given back, ie in constants, storage, extrinsics or runtime API return types. +/// This will recurse deeply into those type IDs to find them. +pub fn collect_return_types( + metadata: &Metadata, + pallets_filter: &mut F, + runtime_apis_filter: &mut G, +) -> impl FnMut(u32) -> bool +where + F: FnMut(&str) -> bool, + G: FnMut(&str) -> bool, +{ + let mut type_set = TypeSet::new(); + for pallet in metadata.pallets.values() { + if pallets_filter(&pallet.name) { + type_set.collect_pallet_types(pallet, metadata); + } + } + for api in metadata.apis.values() { + if runtime_apis_filter(&api.name) { + type_set.collect_runtime_api_types(metadata, api); + } } - update_extrinsic_types(&mut metadata.extrinsic, &map_ids); - update_type(&mut metadata.runtime_ty, &map_ids); - update_runtime_api_types(metadata.apis.values_mut(), &map_ids); + type_set.collect_extrinsic_types(&metadata.extrinsic); + move |type_id| type_set.contains(type_id) } #[cfg(test)] mod tests { use super::*; - use crate::Metadata; + use crate::{Metadata, MetadataHasher}; use codec::Decode; use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed}; use std::{fs, path::Path}; @@ -413,10 +410,9 @@ mod tests { #[test] fn issue_1659() { - let metadata_cache = load_metadata_custom("../artifacts/regressions/1659.scale"); - + let full_metadata = load_metadata_custom("../artifacts/regressions/1659.scale"); // Strip metadata to the pallets as described in the issue. - let mut stripped_metadata = metadata_cache.clone(); + let mut stripped_metadata = full_metadata.clone(); retain_metadata( &mut stripped_metadata, { @@ -431,13 +427,13 @@ mod tests { // check that call_enum did not change as it is referenced inside runtime_api assert_eq!( stripped_metadata.type_hash(stripped_metadata.outer_enums.call_enum_ty), - metadata_cache.type_hash(metadata_cache.outer_enums.call_enum_ty) + full_metadata.type_hash(full_metadata.outer_enums.call_enum_ty) ); // check that event_num did not change as it is referenced inside runtime_api assert_eq!( stripped_metadata.type_hash(stripped_metadata.outer_enums.event_enum_ty), - metadata_cache.type_hash(metadata_cache.outer_enums.event_enum_ty) + full_metadata.type_hash(full_metadata.outer_enums.event_enum_ty) ); } } diff --git a/metadata/src/utils/validation.rs b/metadata/src/utils/validation.rs index 77b072d1e5..11c51ad4d2 100644 --- a/metadata/src/utils/validation.rs +++ b/metadata/src/utils/validation.rs @@ -116,6 +116,7 @@ fn get_variant_hash( fn get_type_def_variant_hash( registry: &PortableRegistry, variant: &TypeDefVariant, + only_these_variants: Option<&[&str]>, cache: &mut HashMap, outer_enum_hashes: &OuterEnumHashes, ) -> Hash { @@ -123,10 +124,18 @@ fn get_type_def_variant_hash( let variant_field_bytes = variant.variants.iter().fold([0u8; HASH_LEN], |bytes, var| { // With EncodeAsType and DecodeAsType we no longer care which order the variants are in, // as long as all of the names+types are there. XOR to not care about ordering. - xor( - bytes, - get_variant_hash(registry, var, cache, outer_enum_hashes), - ) + let should_hash = only_these_variants + .as_ref() + .map(|only_these_variants| only_these_variants.contains(&var.name.as_str())) + .unwrap_or(true); + if should_hash { + xor( + bytes, + get_variant_hash(registry, var, cache, outer_enum_hashes), + ) + } else { + bytes + } }); concat_and_hash2(&variant_id_bytes, &variant_field_bytes) } @@ -156,7 +165,7 @@ fn get_type_def_hash( concat_and_hash2(&composite_id_bytes, &composite_field_bytes) } TypeDef::Variant(variant) => { - get_type_def_variant_hash(registry, variant, cache, outer_enum_hashes) + get_type_def_variant_hash(registry, variant, None, cache, outer_enum_hashes) } TypeDef::Sequence(sequence) => concat_and_hash2( &[TypeBeingHashed::Sequence as u8; HASH_LEN], @@ -578,9 +587,13 @@ impl<'a> MetadataHasher<'a> { pub fn hash(&self) -> Hash { let metadata = self.metadata; - // Get the hashes of outer enums. + // Get the hashes of outer enums, considering only `specific_pallets` (if any are set). // If any of the typed that represent outer enums are encountered later, hashes from `top_level_enum_hashes` can be substituted. - let outer_enum_hashes = OuterEnumHashes::new(metadata); + let outer_enum_hashes = OuterEnumHashes::new( + metadata, + self.specific_pallets.as_deref(), + self.specific_runtime_apis.as_deref(), + ); let pallet_hash = metadata.pallets().fold([0u8; HASH_LEN], |bytes, pallet| { // If specific pallets are given, only include this pallet if it is in the specific pallets. @@ -1095,4 +1108,144 @@ mod tests { to_hash(meta_type::()) ); } + + use frame_metadata::v15::{ + PalletEventMetadata, PalletStorageMetadata, StorageEntryMetadata, StorageEntryModifier, + }; + + fn metadata_with_pallet_events() -> Metadata { + #[allow(dead_code)] + #[derive(scale_info::TypeInfo)] + struct FirstEvent { + s: String, + } + + #[allow(dead_code)] + #[derive(scale_info::TypeInfo)] + struct SecondEvent { + n: u8, + } + + #[allow(dead_code)] + #[derive(scale_info::TypeInfo)] + enum Events { + First(FirstEvent), + Second(SecondEvent), + } + + #[allow(dead_code)] + #[derive(scale_info::TypeInfo)] + enum Errors { + First(DispatchError), + Second(DispatchError), + } + + #[allow(dead_code)] + #[derive(scale_info::TypeInfo)] + enum Calls { + First(u8), + Second(u8), + } + + #[allow(dead_code)] + enum DispatchError { + A, + B, + C, + } + + impl scale_info::TypeInfo for DispatchError { + type Identity = DispatchError; + + fn type_info() -> scale_info::Type { + scale_info::Type { + path: scale_info::Path { + segments: vec!["sp_runtime", "DispatchError"], + }, + type_params: vec![], + type_def: TypeDef::Variant(TypeDefVariant { variants: vec![] }), + docs: vec![], + } + } + } + + let pallets = vec![ + v15::PalletMetadata { + name: "First", + index: 0, + calls: Some(v15::PalletCallMetadata { + ty: meta_type::(), + }), + storage: Some(PalletStorageMetadata { + prefix: "___", + entries: vec![StorageEntryMetadata { + name: "Hello", + modifier: StorageEntryModifier::Optional, + // Note: This is the important part here: + // The Events type will be trimmed down and this trimming needs to be reflected + // when the hash of this storage item is computed. + ty: frame_metadata::v14::StorageEntryType::Plain(meta_type::>()), + default: vec![], + docs: vec![], + }], + }), + event: Some(PalletEventMetadata { + ty: meta_type::(), + }), + constants: vec![], + error: None, + docs: vec![], + }, + v15::PalletMetadata { + name: "Second", + index: 1, + calls: Some(v15::PalletCallMetadata { + ty: meta_type::(), + }), + storage: None, + event: Some(PalletEventMetadata { + ty: meta_type::(), + }), + constants: vec![], + error: None, + docs: vec![], + }, + ]; + + v15::RuntimeMetadataV15::new( + pallets, + build_default_extrinsic(), + meta_type::<()>(), + vec![], + v15::OuterEnums { + call_enum_ty: meta_type::(), + event_enum_ty: meta_type::(), + error_enum_ty: meta_type::(), + }, + v15::CustomMetadata { + map: Default::default(), + }, + ) + .try_into() + .expect("can build valid metadata") + } + + #[test] + fn hash_comparison_trimmed_metadata() { + // trim the metadata: + let metadata = metadata_with_pallet_events(); + let trimmed_metadata = { + let mut m = metadata.clone(); + m.retain(|e| e == "First", |_| true); + m + }; + + // test that the hashes are the same: + let hash = MetadataHasher::new(&metadata) + .only_these_pallets(&["First"]) + .hash(); + let hash_trimmed = MetadataHasher::new(&trimmed_metadata).hash(); + + assert_eq!(hash, hash_trimmed); + } } diff --git a/metadata/src/utils/validation/outer_enum_hashes.rs b/metadata/src/utils/validation/outer_enum_hashes.rs index ebd04f9d13..3e76c1a9d0 100644 --- a/metadata/src/utils/validation/outer_enum_hashes.rs +++ b/metadata/src/utils/validation/outer_enum_hashes.rs @@ -5,7 +5,10 @@ use hashbrown::HashMap; use scale_info::{PortableRegistry, TypeDef}; use crate::{ - utils::validation::{get_type_def_variant_hash, get_type_hash}, + utils::{ + retain, + validation::{get_type_def_variant_hash, get_type_hash}, + }, Metadata, }; @@ -19,9 +22,34 @@ pub struct OuterEnumHashes { } impl OuterEnumHashes { - /// Constructs new `OuterEnumHashes` from metadata. - pub fn new(metadata: &Metadata) -> Self { - fn get_enum_hash(registry: &PortableRegistry, id: u32) -> Hash { + /// Constructs new `OuterEnumHashes` from metadata. If `only_these_variants` is set, the enums are stripped down to only these variants, before their hashes are calculated. + pub fn new( + metadata: &Metadata, + specific_pallets: Option<&[&str]>, + specific_runtimes: Option<&[&str]>, + ) -> Self { + let filter = |names: Option<&[&str]>, name: &str| match names { + Some(names) => names.contains(&name), + None => true, + }; + let mut check_enum_type_id = retain::collect_return_types( + metadata, + &mut |name| filter(specific_pallets, name), + &mut |name| filter(specific_runtimes, name), + ); + + let variants = |filter: bool| { + if !filter { + specific_pallets + } else { + None + } + }; + fn get_enum_hash( + registry: &PortableRegistry, + id: u32, + only_these_variants: Option<&[&str]>, + ) -> Hash { let ty = registry .types .get(id as usize) @@ -31,6 +59,7 @@ impl OuterEnumHashes { get_type_def_variant_hash( registry, variant, + only_these_variants, &mut HashMap::new(), // ignored, because not computed yet... &OuterEnumHashes::empty(), @@ -41,9 +70,12 @@ impl OuterEnumHashes { } let enums = &metadata.outer_enums; - let call_hash = get_enum_hash(metadata.types(), enums.call_enum_ty); - let event_hash = get_enum_hash(metadata.types(), enums.event_enum_ty); - let error_hash = get_enum_hash(metadata.types(), enums.error_enum_ty); + let call_variants = variants(check_enum_type_id(enums.call_enum_ty)); + let call_hash = get_enum_hash(metadata.types(), enums.call_enum_ty, call_variants); + let event_variants = variants(check_enum_type_id(enums.event_enum_ty)); + let event_hash = get_enum_hash(metadata.types(), enums.event_enum_ty, event_variants); + let error_variants = variants(check_enum_type_id(enums.error_enum_ty)); + let error_hash = get_enum_hash(metadata.types(), enums.error_enum_ty, error_variants); Self { call_hash: (enums.call_enum_ty, call_hash), From 2c4156470f67b28c4c78683c199a15cbe561051b Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Fri, 18 Oct 2024 10:27:28 +0200 Subject: [PATCH 14/17] clippy --- metadata/src/utils/retain.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index 877d4f9341..e221b8db62 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -339,7 +339,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{Metadata, MetadataHasher}; + use crate::Metadata; use codec::Decode; use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed}; use std::{fs, path::Path}; From 8f28a7d0d549f002b40b8ddc21451fee6fbdcd3e Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Mon, 21 Oct 2024 13:20:42 +0200 Subject: [PATCH 15/17] review comments --- metadata/src/utils/retain.rs | 70 ++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index e221b8db62..deffb5b46f 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -39,39 +39,41 @@ impl TypeSet { fn collect_types(&mut self, metadata: &Metadata, id: u32) { self.work_set.push_back(id); while let Some(typ) = self.work_set.pop_front() { - if self.insert(typ) { - let typ = resolve_typ(metadata, typ); - match &typ.ty.type_def { - TypeDef::Composite(TypeDefComposite { fields }) => { - for field in fields { - self.work_set.push_back(field.ty.id); - } + if !self.insert(typ) { + // We hit a type we've already inserted; avoid infinite loops and stop. + continue; + } + let typ = resolve_typ(metadata, typ); + match &typ.ty.type_def { + TypeDef::Composite(TypeDefComposite { fields }) => { + for field in fields { + self.work_set.push_back(field.ty.id); } - TypeDef::Variant(TypeDefVariant { variants }) => { - for variant in variants { - for field in &variant.fields { - self.work_set.push_back(field.ty.id); - } + } + TypeDef::Variant(TypeDefVariant { variants }) => { + for variant in variants { + for field in &variant.fields { + self.work_set.push_back(field.ty.id); } } - TypeDef::Array(TypeDefArray { len: _, type_param }) - | TypeDef::Sequence(TypeDefSequence { type_param }) - | TypeDef::Compact(TypeDefCompact { type_param }) => { - self.work_set.push_back(type_param.id); - } - TypeDef::Tuple(TypeDefTuple { fields }) => { - for field in fields { - self.work_set.push_back(field.id); - } + } + TypeDef::Array(TypeDefArray { len: _, type_param }) + | TypeDef::Sequence(TypeDefSequence { type_param }) + | TypeDef::Compact(TypeDefCompact { type_param }) => { + self.work_set.push_back(type_param.id); + } + TypeDef::Tuple(TypeDefTuple { fields }) => { + for field in fields { + self.work_set.push_back(field.id); } - TypeDef::Primitive(_) => (), - TypeDef::BitSequence(TypeDefBitSequence { - bit_store_type, - bit_order_type, - }) => { - for typ in [bit_order_type, bit_store_type] { - self.work_set.push_back(typ.id); - } + } + TypeDef::Primitive(_) => (), + TypeDef::BitSequence(TypeDefBitSequence { + bit_store_type, + bit_order_type, + }) => { + for typ in [bit_order_type, bit_store_type] { + self.work_set.push_back(typ.id); } } } @@ -185,10 +187,8 @@ pub fn retain_metadata( } // 4. Collect all of the type IDs we still want to keep after deleting. - let mut keep_these_type_ids: BTreeSet = iterate_metadata_types(metadata) - .iter() - .map(|x| **x) - .collect(); + let mut keep_these_type_ids: BTreeSet = + iterate_metadata_types(metadata).map(|x| *x).collect(); // 5. Additionally, subxt depends on the `DispatchError` type existing; we use the same // logic here that is used when building our `Metadata` to ensure we keep it too. @@ -237,7 +237,7 @@ where /// return an Vec handing back a mutable reference to each type ID seen in the metadata (not recursively). /// This will iterate over every type referenced in the metadata outside of `metadata.types`. -fn iterate_metadata_types(metadata: &mut Metadata) -> alloc::vec::Vec<&mut u32> { +fn iterate_metadata_types(metadata: &mut Metadata) -> impl Iterator { let mut types = alloc::vec::Vec::new(); // collect outer_enum top-level types @@ -307,7 +307,7 @@ fn iterate_metadata_types(metadata: &mut Metadata) -> alloc::vec::Vec<&mut u32> } } - types + types.into_iter() } /// Look for a type ID anywhere that we can be given back, ie in constants, storage, extrinsics or runtime API return types. From 5732dc154d4cffccb4c4af3af4a3573f2dd3a36a Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko <45178695+pkhry@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:22:34 +0200 Subject: [PATCH 16/17] Update metadata/src/utils/retain.rs Co-authored-by: Niklas Adolfsson --- metadata/src/utils/retain.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index deffb5b46f..d90f68a635 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -235,7 +235,7 @@ where variant.variants.retain(|v| pallets_filter(&v.name)); } -/// return an Vec handing back a mutable reference to each type ID seen in the metadata (not recursively). +/// Returns an iterator that allows modifying each type ID seen in the metadata (not recursively). /// This will iterate over every type referenced in the metadata outside of `metadata.types`. fn iterate_metadata_types(metadata: &mut Metadata) -> impl Iterator { let mut types = alloc::vec::Vec::new(); From 35c43a28f04cfee8978ae3c70d9ba73ed4e0c3fc Mon Sep 17 00:00:00 2001 From: Pavlo Khrystenko Date: Mon, 21 Oct 2024 22:16:41 +0200 Subject: [PATCH 17/17] review comments --- metadata/src/utils/retain.rs | 39 +++++++++++-------- .../src/utils/validation/outer_enum_hashes.rs | 2 +- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/metadata/src/utils/retain.rs b/metadata/src/utils/retain.rs index d90f68a635..4e13982a69 100644 --- a/metadata/src/utils/retain.rs +++ b/metadata/src/utils/retain.rs @@ -7,7 +7,8 @@ use crate::{ ExtrinsicMetadata, Metadata, PalletMetadataInner, RuntimeApiMetadataInner, StorageEntryType, }; -use alloc::collections::{BTreeSet, VecDeque}; +use alloc::collections::BTreeSet; +use alloc::vec::Vec; use scale_info::{ PortableType, TypeDef, TypeDefArray, TypeDefBitSequence, TypeDefCompact, TypeDefComposite, TypeDefSequence, TypeDefTuple, TypeDefVariant, @@ -16,14 +17,15 @@ use scale_info::{ #[derive(Clone)] struct TypeSet { seen_ids: BTreeSet, - work_set: VecDeque, + pub work_set: Vec, } impl TypeSet { fn new() -> Self { Self { seen_ids: BTreeSet::new(), - work_set: VecDeque::with_capacity(22), + // Average work set size is around 30-50 elements, depending on the metadata size + work_set: Vec::with_capacity(32), } } @@ -35,36 +37,39 @@ impl TypeSet { self.seen_ids.contains(&id) } + fn push_to_workset(&mut self, id: u32) { + // Check if wee hit a type we've already inserted; avoid infinite loops and stop. + if self.insert(id) { + self.work_set.push(id); + } + } + /// This function will deeply traverse the inital type and it's dependencies to collect the relevant type_ids fn collect_types(&mut self, metadata: &Metadata, id: u32) { - self.work_set.push_back(id); - while let Some(typ) = self.work_set.pop_front() { - if !self.insert(typ) { - // We hit a type we've already inserted; avoid infinite loops and stop. - continue; - } + self.push_to_workset(id); + while let Some(typ) = self.work_set.pop() { let typ = resolve_typ(metadata, typ); match &typ.ty.type_def { TypeDef::Composite(TypeDefComposite { fields }) => { for field in fields { - self.work_set.push_back(field.ty.id); + self.push_to_workset(field.ty.id); } } TypeDef::Variant(TypeDefVariant { variants }) => { for variant in variants { for field in &variant.fields { - self.work_set.push_back(field.ty.id); + self.push_to_workset(field.ty.id); } } } TypeDef::Array(TypeDefArray { len: _, type_param }) | TypeDef::Sequence(TypeDefSequence { type_param }) | TypeDef::Compact(TypeDefCompact { type_param }) => { - self.work_set.push_back(type_param.id); + self.push_to_workset(type_param.id); } TypeDef::Tuple(TypeDefTuple { fields }) => { for field in fields { - self.work_set.push_back(field.id); + self.push_to_workset(field.id); } } TypeDef::Primitive(_) => (), @@ -73,7 +78,7 @@ impl TypeSet { bit_order_type, }) => { for typ in [bit_order_type, bit_store_type] { - self.work_set.push_back(typ.id); + self.push_to_workset(typ.id); } } } @@ -174,8 +179,7 @@ pub fn retain_metadata( // of the things we're keeping (because if it is, we need to keep all of it so that we // can still decode values into it). let outer_enums = metadata.outer_enums(); - let mut find_type_id = - collect_return_types(metadata, &mut pallets_filter, &mut runtime_apis_filter); + let mut find_type_id = keep_outer_enum(metadata, &mut pallets_filter, &mut runtime_apis_filter); for outer_enum_ty_id in [ outer_enums.call_enum_ty(), outer_enums.error_enum_ty(), @@ -312,7 +316,7 @@ fn iterate_metadata_types(metadata: &mut Metadata) -> impl Iterator( +pub fn keep_outer_enum( metadata: &Metadata, pallets_filter: &mut F, runtime_apis_filter: &mut G, @@ -373,6 +377,7 @@ mod tests { |pallet_name| pallet_name == pallet.name(), |_| true, ); + assert_eq!(metadata.pallets.len(), 1); assert_eq!( &*metadata.pallets.get_by_index(0).unwrap().name, diff --git a/metadata/src/utils/validation/outer_enum_hashes.rs b/metadata/src/utils/validation/outer_enum_hashes.rs index 3e76c1a9d0..9144c6b6a6 100644 --- a/metadata/src/utils/validation/outer_enum_hashes.rs +++ b/metadata/src/utils/validation/outer_enum_hashes.rs @@ -32,7 +32,7 @@ impl OuterEnumHashes { Some(names) => names.contains(&name), None => true, }; - let mut check_enum_type_id = retain::collect_return_types( + let mut check_enum_type_id = retain::keep_outer_enum( metadata, &mut |name| filter(specific_pallets, name), &mut |name| filter(specific_runtimes, name),