// Copyright 2019-2025 Parity Technologies (UK) Ltd. // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. //! Utility functions for metadata validation. use crate::{ CustomMetadata, CustomValueMetadata, ExtrinsicMetadata, Metadata, PalletMetadata, RuntimeApiMetadata, RuntimeApiMethodMetadata, StorageEntryMetadata, ViewFunctionMetadata, }; use alloc::vec::Vec; use hashbrown::HashMap; use scale_info::{Field, PortableRegistry, TypeDef, TypeDefVariant, Variant, form::PortableForm}; // The number of bytes our `hash` function produces. pub(crate) const HASH_LEN: usize = 32; pub type Hash = [u8; HASH_LEN]; /// Internal byte representation for various metadata types utilized for /// generating deterministic hashes between different rust versions. #[repr(u8)] enum TypeBeingHashed { Composite, Variant, Sequence, Array, Tuple, Primitive, Compact, BitSequence, } /// Hashing function utilized internally. fn hash(data: &[u8]) -> Hash { sp_crypto_hashing::twox_256(data) } /// XOR two hashes together. Only use this when you don't care about the order /// of the things you're hashing together. fn xor(a: Hash, b: Hash) -> Hash { let mut out = [0u8; HASH_LEN]; for (idx, (a, b)) in a.into_iter().zip(b).enumerate() { out[idx] = a ^ b; } out } // Combine some number of HASH_LEN byte hashes and output a single HASH_LEN // byte hash to uniquely represent the inputs. macro_rules! count_idents { () => { 0 }; ($n:ident $($rest:ident)*) => { 1 + count_idents!($($rest)*) } } macro_rules! concat_and_hash_n { ($name:ident($($arg:ident)+)) => { fn $name($($arg: &Hash),+) -> Hash { let mut out = [0u8; HASH_LEN * count_idents!($($arg)+)]; let mut start = 0; $( out[start..start+HASH_LEN].copy_from_slice(&$arg[..]); #[allow(unused_assignments)] { start += HASH_LEN; } )+ hash(&out) } } } concat_and_hash_n!(concat_and_hash2(a b)); concat_and_hash_n!(concat_and_hash3(a b c)); concat_and_hash_n!(concat_and_hash4(a b c d)); concat_and_hash_n!(concat_and_hash5(a b c d e)); concat_and_hash_n!(concat_and_hash6(a b c d e f)); /// Obtain the hash representation of a `scale_info::Field`. fn get_field_hash( registry: &PortableRegistry, field: &Field, cache: &mut HashMap, ) -> Hash { let field_name_bytes = match &field.name { Some(name) => hash(name.as_bytes()), None => [0u8; HASH_LEN], }; concat_and_hash2( &field_name_bytes, &get_type_hash_recurse(registry, field.ty.id, cache), ) } /// Obtain the hash representation of a `scale_info::Variant`. fn get_variant_hash( registry: &PortableRegistry, var: &Variant, cache: &mut HashMap, ) -> Hash { let variant_name_bytes = hash(var.name.as_bytes()); let variant_field_bytes = var.fields.iter().fold([0u8; HASH_LEN], |bytes, field| { // EncodeAsType and DecodeAsType don't care about variant field ordering, // so XOR the fields to ensure that it doesn't matter. xor(bytes, get_field_hash(registry, field, cache)) }); concat_and_hash2(&variant_name_bytes, &variant_field_bytes) } fn get_type_def_variant_hash( registry: &PortableRegistry, variant: &TypeDefVariant, only_these_variants: Option<&[&str]>, cache: &mut HashMap, ) -> Hash { let variant_id_bytes = [TypeBeingHashed::Variant as u8; HASH_LEN]; 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); if should_hash { xor(bytes, get_variant_hash(registry, var, cache)) } else { bytes } }); concat_and_hash2(&variant_id_bytes, &variant_field_bytes) } /// Obtain the hash representation of a `scale_info::TypeDef`. fn get_type_def_hash( registry: &PortableRegistry, ty_def: &TypeDef, cache: &mut HashMap, ) -> Hash { match ty_def { TypeDef::Composite(composite) => { let composite_id_bytes = [TypeBeingHashed::Composite as u8; HASH_LEN]; let composite_field_bytes = composite .fields .iter() .fold([0u8; HASH_LEN], |bytes, field| { // With EncodeAsType and DecodeAsType we no longer care which order the fields are in, // as long as all of the names+types are there. XOR to not care about ordering. xor(bytes, get_field_hash(registry, field, cache)) }); concat_and_hash2(&composite_id_bytes, &composite_field_bytes) } TypeDef::Variant(variant) => get_type_def_variant_hash(registry, variant, None, cache), TypeDef::Sequence(sequence) => concat_and_hash2( &[TypeBeingHashed::Sequence as u8; HASH_LEN], &get_type_hash_recurse(registry, sequence.type_param.id, cache), ), TypeDef::Array(array) => { // Take length into account too; different length must lead to different hash. let array_id_bytes = { let mut a = [0u8; HASH_LEN]; a[0] = TypeBeingHashed::Array as u8; a[1..5].copy_from_slice(&array.len.to_be_bytes()); a }; concat_and_hash2( &array_id_bytes, &get_type_hash_recurse(registry, array.type_param.id, cache), ) } TypeDef::Tuple(tuple) => { let mut bytes = hash(&[TypeBeingHashed::Tuple as u8]); for field in &tuple.fields { bytes = concat_and_hash2(&bytes, &get_type_hash_recurse(registry, field.id, cache)); } bytes } TypeDef::Primitive(primitive) => { // Cloning the 'primitive' type should essentially be a copy. hash(&[TypeBeingHashed::Primitive as u8, primitive.clone() as u8]) } TypeDef::Compact(compact) => concat_and_hash2( &[TypeBeingHashed::Compact as u8; HASH_LEN], &get_type_hash_recurse(registry, compact.type_param.id, cache), ), TypeDef::BitSequence(bitseq) => concat_and_hash3( &[TypeBeingHashed::BitSequence as u8; HASH_LEN], &get_type_hash_recurse(registry, bitseq.bit_order_type.id, cache), &get_type_hash_recurse(registry, bitseq.bit_store_type.id, cache), ), } } /// indicates whether a hash has been fully computed for a type or not #[derive(Clone, Debug)] pub enum CachedHash { /// hash not known yet, but computation has already started Recursive, /// hash of the type, computation was finished Hash(Hash), } impl CachedHash { fn hash(&self) -> Hash { match &self { CachedHash::Hash(hash) => *hash, CachedHash::Recursive => [123; HASH_LEN], // some magical value } } } /// Obtain the hash representation of a `scale_info::Type` identified by id. /// /// Hashes of the outer enums (call, event, error) should be computed prior to this /// and passed in as the `outer_enum_hashes` argument. Whenever a type is encountered that /// is one of the outer enums, the procomputed hash is used instead of computing a new one. /// /// The reason for this unintuitive behavior is that we sometimes want to trim the outer enum types /// beforehand to only include certain pallets, which affects their hash values. pub fn get_type_hash(registry: &PortableRegistry, id: u32) -> Hash { get_type_hash_recurse(registry, id, &mut HashMap::new()) } /// Obtain the hash representation of a `scale_info::Type` identified by id. fn get_type_hash_recurse( registry: &PortableRegistry, id: u32, cache: &mut HashMap, ) -> Hash { // Guard against recursive types, with a 2 step caching approach: // if the cache has an entry for the id, just return a hash derived from it. // if the type has not been seen yet, mark it with `CachedHash::Recursive` in the cache and proceed to `get_type_def_hash()`. // -> During the execution of get_type_def_hash() we might get into get_type_hash(id) again for the original id // -> in this case the `CachedHash::Recursive` provokes an early return. // -> Once we return from `get_type_def_hash()` we need to update the cache entry: // -> We set the cache value to `CachedHash::Hash(type_hash)`, where `type_hash` was returned from `get_type_def_hash()` // -> It makes sure, that different types end up with different cache values. // // Values in the cache can be thought of as a mapping like this: // type_id -> not contained = We haven't seen the type yet. // -> `CachedHash::Recursive` = We have seen the type but hash calculation for it hasn't finished yet. // -> `CachedHash::Hash(hash)` = Hash calculation for the type was completed. if let Some(cached_hash) = cache.get(&id) { return cached_hash.hash(); } cache.insert(id, CachedHash::Recursive); let ty = registry .resolve(id) .expect("Type ID provided by the metadata is registered; qed"); let type_hash = get_type_def_hash(registry, &ty.type_def, cache); cache.insert(id, CachedHash::Hash(type_hash)); type_hash } /// Obtain the hash representation of a `frame_metadata::v15::ExtrinsicMetadata`. fn get_extrinsic_hash(registry: &PortableRegistry, extrinsic: &ExtrinsicMetadata) -> Hash { // Get the hashes of the extrinsic type. let address_hash = get_type_hash(registry, extrinsic.address_ty); // The `RuntimeCall` type is intentionally omitted and hashed by the outer enums instead. let signature_hash = get_type_hash(registry, extrinsic.signature_ty); // Supported versions are just u8s and we will likely never have more than 32 of these, so put them into // an array of u8s and panic if more than 32. if extrinsic.supported_versions.len() > 32 { panic!("The metadata validation logic does not support more than 32 extrinsic versions."); } let supported_extrinsic_versions = { let mut a = [0u8; 32]; a[0..extrinsic.supported_versions.len()].copy_from_slice(&extrinsic.supported_versions); a }; let mut bytes = concat_and_hash3( &address_hash, &signature_hash, &supported_extrinsic_versions, ); for signed_extension in extrinsic.transaction_extensions.iter() { bytes = concat_and_hash4( &bytes, &hash(signed_extension.identifier.as_bytes()), &get_type_hash(registry, signed_extension.extra_ty), &get_type_hash(registry, signed_extension.additional_ty), ) } bytes } /// Get the hash corresponding to a single storage entry. fn get_storage_entry_hash(registry: &PortableRegistry, entry: &StorageEntryMetadata) -> Hash { let mut bytes = concat_and_hash3( &hash(entry.name.as_bytes()), &get_type_hash(registry, entry.info.value_id), &hash(entry.info.default_value.as_ref().map(|b| &**b).unwrap_or_default()) ); for key in &*entry.info.keys { bytes = concat_and_hash3( &bytes, &[key.hasher as u8; HASH_LEN], &get_type_hash(registry, key.key_id), ) } bytes } fn get_custom_metadata_hash(custom_metadata: &CustomMetadata) -> Hash { custom_metadata .iter() .fold([0u8; HASH_LEN], |bytes, custom_value| { xor(bytes, get_custom_value_hash(&custom_value)) }) } /// Obtain the hash of some custom value in the metadata including it's name/key. /// /// If the `custom_value` has a type id that is not present in the metadata, /// only the name and bytes are used for hashing. pub fn get_custom_value_hash(custom_value: &CustomValueMetadata) -> Hash { let name_hash = hash(custom_value.name.as_bytes()); if custom_value.types.resolve(custom_value.type_id()).is_none() { hash(&name_hash) } else { concat_and_hash2( &name_hash, &get_type_hash(custom_value.types, custom_value.type_id()), ) } } /// Obtain the hash for a specific storage item, or an error if it's not found. pub fn get_storage_hash(pallet: &PalletMetadata, entry_name: &str) -> Option { let storage = pallet.storage()?; let entry = storage.entry_by_name(entry_name)?; let hash = get_storage_entry_hash(pallet.types, entry); Some(hash) } /// Obtain the hash for a specific constant, or an error if it's not found. pub fn get_constant_hash(pallet: &PalletMetadata, constant_name: &str) -> Option { let constant = pallet.constant_by_name(constant_name)?; // We only need to check that the type of the constant asked for matches. let bytes = get_type_hash(pallet.types, constant.ty); Some(bytes) } /// Obtain the hash for a specific call, or an error if it's not found. pub fn get_call_hash(pallet: &PalletMetadata, call_name: &str) -> Option { let call_variant = pallet.call_variant_by_name(call_name)?; // hash the specific variant representing the call we are interested in. let hash = get_variant_hash(pallet.types, call_variant, &mut HashMap::new()); Some(hash) } /// Obtain the hash of a specific runtime API method, or an error if it's not found. pub fn get_runtime_api_hash(runtime_api: &RuntimeApiMethodMetadata) -> Hash { let registry = runtime_api.types; // The trait name is part of the runtime API call that is being // generated for this method. Therefore the trait name is strongly // connected to the method in the same way as a parameter is // to the method. let mut bytes = concat_and_hash2( &hash(runtime_api.trait_name.as_bytes()), &hash(runtime_api.name().as_bytes()), ); for input in runtime_api.inputs() { bytes = concat_and_hash3( &bytes, &hash(input.name.as_bytes()), &get_type_hash(registry, input.id), ); } bytes = concat_and_hash2(&bytes, &get_type_hash(registry, runtime_api.output_ty())); bytes } /// Obtain the hash of all of a runtime API trait, including all of its methods. pub fn get_runtime_apis_hash(trait_metadata: RuntimeApiMetadata) -> Hash { // Each API is already hashed considering the trait name, so we don't need // to consider thr trait name again here. trait_metadata .methods() .fold([0u8; HASH_LEN], |bytes, method_metadata| { // We don't care what order the trait methods exist in, and want the hash to // be identical regardless. For this, we can just XOR the hashes for each method // together; we'll get the same output whichever order they are XOR'd together in, // so long as each individual method is the same. xor(bytes, get_runtime_api_hash(&method_metadata)) }) } /// Obtain the hash of a specific view function, or an error if it's not found. pub fn get_view_function_hash(view_function: &ViewFunctionMetadata) -> Hash { let registry = view_function.types; // The Query ID is `twox_128(pallet_name) ++ twox_128("fn_name(fnarg_types) -> return_ty")`. let mut bytes = *view_function.query_id(); // This only takes type _names_ into account, so we beef this up by combining with actual // type hashes, in a similar approach to runtime APIs.. for input in view_function.inputs() { bytes = concat_and_hash3( &bytes, &hash(input.name.as_bytes()), &get_type_hash(registry, input.id), ); } bytes = concat_and_hash2(&bytes, &get_type_hash(registry, view_function.output_ty())); bytes } /// Obtain the hash of all of the view functions in a pallet, including all of its methods. fn get_pallet_view_functions_hash(pallet_metadata: &PalletMetadata) -> Hash { // Each API is already hashed considering the trait name, so we don't need // to consider thr trait name again here. pallet_metadata .view_functions() .fold([0u8; HASH_LEN], |bytes, method_metadata| { // We don't care what order the view functions are declared in, and want the hash to // be identical regardless. For this, we can just XOR the hashes for each method // together; we'll get the same output whichever order they are XOR'd together in, // so long as each individual method is the same. xor(bytes, get_view_function_hash(&method_metadata)) }) } /// Obtain the hash representation of a `frame_metadata::v15::PalletMetadata`. pub fn get_pallet_hash(pallet: PalletMetadata) -> Hash { let registry = pallet.types; let call_bytes = match pallet.call_ty_id() { Some(calls) => get_type_hash(registry, calls), None => [0u8; HASH_LEN], }; let event_bytes = match pallet.event_ty_id() { Some(event) => get_type_hash(registry, event), None => [0u8; HASH_LEN], }; let error_bytes = match pallet.error_ty_id() { Some(error) => get_type_hash(registry, error), None => [0u8; HASH_LEN], }; let constant_bytes = pallet.constants().fold([0u8; HASH_LEN], |bytes, constant| { // We don't care what order the constants occur in, so XOR together the combinations // of (constantName, constantType) to make the order we see them irrelevant. let constant_hash = concat_and_hash2( &hash(constant.name.as_bytes()), &get_type_hash(registry, constant.ty()), ); xor(bytes, constant_hash) }); let storage_bytes = match pallet.storage() { Some(storage) => { let prefix_hash = hash(storage.prefix().as_bytes()); let entries_hash = storage .entries() .iter() .fold([0u8; HASH_LEN], |bytes, entry| { // We don't care what order the storage entries occur in, so XOR them together // to make the order irrelevant. xor(bytes, get_storage_entry_hash(registry, entry)) }); concat_and_hash2(&prefix_hash, &entries_hash) } None => [0u8; HASH_LEN], }; let view_functions_bytes = get_pallet_view_functions_hash(&pallet); // Hash all of the above together: concat_and_hash6( &call_bytes, &event_bytes, &error_bytes, &constant_bytes, &storage_bytes, &view_functions_bytes, ) } /// Obtain a hash representation of our metadata or some part of it. /// This is obtained by calling [`crate::Metadata::hasher()`]. pub struct MetadataHasher<'a> { metadata: &'a Metadata, specific_pallets: Option>, specific_runtime_apis: Option>, include_custom_values: bool, } impl<'a> MetadataHasher<'a> { /// Create a new [`MetadataHasher`] pub(crate) fn new(metadata: &'a Metadata) -> Self { Self { metadata, specific_pallets: None, specific_runtime_apis: None, include_custom_values: true, } } /// Only hash the provided pallets instead of hashing every pallet. pub fn only_these_pallets>(&mut self, specific_pallets: &'a [S]) -> &mut Self { self.specific_pallets = Some(specific_pallets.iter().map(|n| n.as_ref()).collect()); self } /// Only hash the provided runtime APIs instead of hashing every runtime API pub fn only_these_runtime_apis>( &mut self, specific_runtime_apis: &'a [S], ) -> &mut Self { self.specific_runtime_apis = Some(specific_runtime_apis.iter().map(|n| n.as_ref()).collect()); self } /// Do not hash the custom values pub fn ignore_custom_values(&mut self) -> &mut Self { self.include_custom_values = false; self } /// Hash the given metadata. pub fn hash(&self) -> Hash { let metadata = self.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. let should_hash = self .specific_pallets .as_ref() .map(|specific_pallets| specific_pallets.contains(&pallet.name())) .unwrap_or(true); // We don't care what order the pallets are seen in, so XOR their // hashes together to be order independent. if should_hash { xor(bytes, get_pallet_hash(pallet)) } else { bytes } }); let apis_hash = metadata .runtime_api_traits() .fold([0u8; HASH_LEN], |bytes, api| { // If specific runtime APIs are given, only include this pallet if it is in the specific runtime APIs. let should_hash = self .specific_runtime_apis .as_ref() .map(|specific_runtime_apis| specific_runtime_apis.contains(&api.name())) .unwrap_or(true); // We don't care what order the runtime APIs are seen in, so XOR their // hashes together to be order independent. if should_hash { xor(bytes, get_runtime_apis_hash(api)) } else { bytes } }); let outer_enums_hash = concat_and_hash3( &get_type_hash(&metadata.types, metadata.outer_enums.call_enum_ty), &get_type_hash(&metadata.types, metadata.outer_enums.event_enum_ty), &get_type_hash(&metadata.types, metadata.outer_enums.error_enum_ty), ); let extrinsic_hash = get_extrinsic_hash(&metadata.types, &metadata.extrinsic); let custom_values_hash = if self.include_custom_values { get_custom_metadata_hash(&metadata.custom()) } else { Default::default() }; concat_and_hash5( &pallet_hash, &apis_hash, &outer_enums_hash, &extrinsic_hash, &custom_values_hash, ) } } #[cfg(test)] mod tests { use super::*; use bitvec::{order::Lsb0, vec::BitVec}; use frame_metadata::v15; use scale_info::{Registry, meta_type}; // Define recursive types. #[allow(dead_code)] #[derive(scale_info::TypeInfo)] struct A { pub b: Box, } #[allow(dead_code)] #[derive(scale_info::TypeInfo)] struct B { pub a: Box, } // Define TypeDef supported types. #[allow(dead_code)] #[derive(scale_info::TypeInfo)] // TypeDef::Composite with TypeDef::Array with Typedef::Primitive. struct AccountId32(Hash); #[allow(dead_code)] #[derive(scale_info::TypeInfo)] // TypeDef::Variant. enum DigestItem { PreRuntime( // TypeDef::Array with primitive. [::core::primitive::u8; 4usize], // TypeDef::Sequence. ::std::vec::Vec<::core::primitive::u8>, ), Other(::std::vec::Vec<::core::primitive::u8>), // Nested TypeDef::Tuple. RuntimeEnvironmentUpdated(((i8, i16), (u32, u64))), // TypeDef::Compact. Index(#[codec(compact)] ::core::primitive::u8), // TypeDef::BitSequence. BitSeq(BitVec), } #[allow(dead_code)] #[derive(scale_info::TypeInfo)] // Ensure recursive types and TypeDef variants are captured. struct MetadataTestType { recursive: A, composite: AccountId32, type_def: DigestItem, } #[allow(dead_code)] #[derive(scale_info::TypeInfo)] // Simulate a PalletCallMetadata. enum Call { #[codec(index = 0)] FillBlock { ratio: AccountId32 }, #[codec(index = 1)] Remark { remark: DigestItem }, } fn build_default_extrinsic() -> v15::ExtrinsicMetadata { v15::ExtrinsicMetadata { version: 0, signed_extensions: vec![], address_ty: meta_type::<()>(), call_ty: meta_type::<()>(), signature_ty: meta_type::<()>(), extra_ty: meta_type::<()>(), } } fn default_pallet() -> v15::PalletMetadata { v15::PalletMetadata { name: "Test", storage: None, calls: None, event: None, constants: vec![], error: None, index: 0, docs: vec![], } } fn build_default_pallets() -> Vec { vec![ v15::PalletMetadata { name: "First", calls: Some(v15::PalletCallMetadata { ty: meta_type::(), }), ..default_pallet() }, v15::PalletMetadata { name: "Second", index: 1, calls: Some(v15::PalletCallMetadata { ty: meta_type::<(DigestItem, AccountId32, A)>(), }), ..default_pallet() }, ] } fn pallets_to_metadata(pallets: Vec) -> Metadata { 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 different_pallet_index() { let pallets = build_default_pallets(); let mut pallets_swap = pallets.clone(); let metadata = pallets_to_metadata(pallets); // Change the order in which pallets are registered. pallets_swap.swap(0, 1); pallets_swap[0].index = 0; pallets_swap[1].index = 1; let metadata_swap = pallets_to_metadata(pallets_swap); let hash = MetadataHasher::new(&metadata).hash(); let hash_swap = MetadataHasher::new(&metadata_swap).hash(); // Changing pallet order must still result in a deterministic unique hash. assert_eq!(hash, hash_swap); } #[test] fn recursive_type() { let mut pallet = default_pallet(); pallet.calls = Some(v15::PalletCallMetadata { ty: meta_type::(), }); let metadata = pallets_to_metadata(vec![pallet]); // Check hashing algorithm finishes on a recursive type. MetadataHasher::new(&metadata).hash(); } #[test] /// Ensure correctness of hashing when parsing the `metadata.types`. /// /// Having a recursive structure `A: { B }` and `B: { A }` registered in different order /// `types: { { id: 0, A }, { id: 1, B } }` and `types: { { id: 0, B }, { id: 1, A } }` /// must produce the same deterministic hashing value. fn recursive_types_different_order() { let mut pallets = build_default_pallets(); pallets[0].calls = Some(v15::PalletCallMetadata { ty: meta_type::(), }); pallets[1].calls = Some(v15::PalletCallMetadata { ty: meta_type::(), }); pallets[1].index = 1; let mut pallets_swap = pallets.clone(); let metadata = pallets_to_metadata(pallets); pallets_swap.swap(0, 1); pallets_swap[0].index = 0; pallets_swap[1].index = 1; let metadata_swap = pallets_to_metadata(pallets_swap); let hash = MetadataHasher::new(&metadata).hash(); let hash_swap = MetadataHasher::new(&metadata_swap).hash(); // Changing pallet order must still result in a deterministic unique hash. assert_eq!(hash, hash_swap); } #[allow(dead_code)] #[derive(scale_info::TypeInfo)] struct Aba { ab: (A, B), other: A, } #[allow(dead_code)] #[derive(scale_info::TypeInfo)] struct Abb { ab: (A, B), other: B, } #[test] /// Ensure ABB and ABA have a different structure: fn do_not_reuse_visited_type_ids() { let metadata_hash_with_type = |ty| { let mut pallets = build_default_pallets(); pallets[0].calls = Some(v15::PalletCallMetadata { ty }); let metadata = pallets_to_metadata(pallets); MetadataHasher::new(&metadata).hash() }; let aba_hash = metadata_hash_with_type(meta_type::()); let abb_hash = metadata_hash_with_type(meta_type::()); assert_ne!(aba_hash, abb_hash); } #[test] fn hash_cache_gets_filled_with_correct_hashes() { let mut registry = Registry::new(); let a_type_id = registry.register_type(&meta_type::()).id; let b_type_id = registry.register_type(&meta_type::()).id; let registry: PortableRegistry = registry.into(); let mut cache = HashMap::new(); let a_hash = get_type_hash_recurse(®istry, a_type_id, &mut cache); let a_hash2 = get_type_hash_recurse(®istry, a_type_id, &mut cache); let b_hash = get_type_hash_recurse(®istry, b_type_id, &mut cache); let CachedHash::Hash(a_cache_hash) = cache[&a_type_id] else { panic!() }; let CachedHash::Hash(b_cache_hash) = cache[&b_type_id] else { panic!() }; assert_eq!(a_hash, a_cache_hash); assert_eq!(b_hash, b_cache_hash); assert_eq!(a_hash, a_hash2); assert_ne!(a_hash, b_hash); } #[test] // Redundant clone clippy warning is a lie; https://github.com/rust-lang/rust-clippy/issues/10870 #[allow(clippy::redundant_clone)] fn pallet_hash_correctness() { let compare_pallets_hash = |lhs: &v15::PalletMetadata, rhs: &v15::PalletMetadata| { let metadata = pallets_to_metadata(vec![lhs.clone()]); let hash = MetadataHasher::new(&metadata).hash(); let metadata = pallets_to_metadata(vec![rhs.clone()]); let new_hash = MetadataHasher::new(&metadata).hash(); assert_ne!(hash, new_hash); }; // Build metadata progressively from an empty pallet to a fully populated pallet. let mut pallet = default_pallet(); let pallet_lhs = pallet.clone(); pallet.storage = Some(v15::PalletStorageMetadata { prefix: "Storage", entries: vec![v15::StorageEntryMetadata { name: "BlockWeight", modifier: v15::StorageEntryModifier::Default, ty: v15::StorageEntryType::Plain(meta_type::()), default: vec![], docs: vec![], }], }); compare_pallets_hash(&pallet_lhs, &pallet); let pallet_lhs = pallet.clone(); // Calls are similar to: // // ``` // pub enum Call { // call_name_01 { arg01: type }, // call_name_02 { arg01: type, arg02: type } // } // ``` pallet.calls = Some(v15::PalletCallMetadata { ty: meta_type::(), }); compare_pallets_hash(&pallet_lhs, &pallet); let pallet_lhs = pallet.clone(); // Events are similar to Calls. pallet.event = Some(v15::PalletEventMetadata { ty: meta_type::(), }); compare_pallets_hash(&pallet_lhs, &pallet); let pallet_lhs = pallet.clone(); pallet.constants = vec![v15::PalletConstantMetadata { name: "BlockHashCount", ty: meta_type::(), value: vec![96u8, 0, 0, 0], docs: vec![], }]; compare_pallets_hash(&pallet_lhs, &pallet); let pallet_lhs = pallet.clone(); pallet.error = Some(v15::PalletErrorMetadata { ty: meta_type::(), }); compare_pallets_hash(&pallet_lhs, &pallet); } #[test] fn metadata_per_pallet_hash_correctness() { let pallets = build_default_pallets(); // Build metadata with just the first pallet. let metadata_one = pallets_to_metadata(vec![pallets[0].clone()]); // Build metadata with both pallets. let metadata_both = pallets_to_metadata(pallets); // Hashing will ignore any non-existant pallet and return the same result. let hash = MetadataHasher::new(&metadata_one) .only_these_pallets(&["First", "Second"]) .hash(); let hash_rhs = MetadataHasher::new(&metadata_one) .only_these_pallets(&["First"]) .hash(); assert_eq!(hash, hash_rhs, "hashing should ignore non-existant pallets"); // Hashing one pallet from metadata with 2 pallets inserted will ignore the second pallet. let hash_second = MetadataHasher::new(&metadata_both) .only_these_pallets(&["First"]) .hash(); assert_eq!( hash_second, hash, "hashing one pallet should ignore the others" ); // Check hashing with all pallets. let hash_second = MetadataHasher::new(&metadata_both) .only_these_pallets(&["First", "Second"]) .hash(); assert_ne!( hash_second, hash, "hashing both pallets should produce a different result from hashing just one pallet" ); } #[test] fn field_semantic_changes() { // Get a hash representation of the provided meta type, // inserted in the context of pallet metadata call. let to_hash = |meta_ty| { let pallet = v15::PalletMetadata { calls: Some(v15::PalletCallMetadata { ty: meta_ty }), ..default_pallet() }; let metadata = pallets_to_metadata(vec![pallet]); MetadataHasher::new(&metadata).hash() }; #[allow(dead_code)] #[derive(scale_info::TypeInfo)] enum EnumA1 { First { hi: u8, bye: String }, Second(u32), Third, } #[allow(dead_code)] #[derive(scale_info::TypeInfo)] enum EnumA2 { Second(u32), Third, First { bye: String, hi: u8 }, } // EncodeAsType and DecodeAsType only care about enum variant names // and not indexes or field ordering or the enum name itself.. assert_eq!( to_hash(meta_type::()), to_hash(meta_type::()) ); #[allow(dead_code)] #[derive(scale_info::TypeInfo)] struct StructB1 { hello: bool, another: [u8; 32], } #[allow(dead_code)] #[derive(scale_info::TypeInfo)] struct StructB2 { another: [u8; 32], hello: bool, } // As with enums, struct names and field orders are irrelevant as long as // the field names and types are the same. assert_eq!( to_hash(meta_type::()), to_hash(meta_type::()) ); #[allow(dead_code)] #[derive(scale_info::TypeInfo)] enum EnumC1 { First(u8), } #[allow(dead_code)] #[derive(scale_info::TypeInfo)] enum EnumC2 { Second(u8), } // The enums are binary compatible, but the variants have different names, so // semantically they are different and should not be equal. assert_ne!( to_hash(meta_type::()), to_hash(meta_type::()) ); #[allow(dead_code)] #[derive(scale_info::TypeInfo)] enum EnumD1 { First { a: u8 }, } #[allow(dead_code)] #[derive(scale_info::TypeInfo)] enum EnumD2 { First { b: u8 }, } // Named fields contain a different semantic meaning ('a' and 'b') despite // being binary compatible, so hashes should be different. assert_ne!( to_hash(meta_type::()), to_hash(meta_type::()) ); #[allow(dead_code)] #[derive(scale_info::TypeInfo)] struct StructE1 { a: u32, } #[allow(dead_code)] #[derive(scale_info::TypeInfo)] struct StructE2 { b: u32, } // Similar to enums, struct fields that contain a different semantic meaning // ('a' and 'b') despite being binary compatible will have different hashes. assert_ne!( to_hash(meta_type::()), to_hash(meta_type::()) ); } use frame_metadata::v15::{ PalletEventMetadata, PalletStorageMetadata, StorageEntryMetadata, StorageEntryModifier, }; fn metadata_with_pallet_events() -> v15::RuntimeMetadataV15 { #[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(), }, ) } #[test] fn hash_comparison_trimmed_metadata() { use subxt_utils_stripmetadata::StripMetadata; // trim the metadata: let metadata = metadata_with_pallet_events(); let trimmed_metadata = { let mut m = metadata.clone(); m.strip_metadata(|e| e == "First", |_| true); m }; // Now convert it into our inner repr: let metadata = Metadata::try_from(metadata).unwrap(); let trimmed_metadata = Metadata::try_from(trimmed_metadata).unwrap(); // 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); } }