Skip to content

Commit

Permalink
Add proper test Custom values (#1147)
Browse files Browse the repository at this point in the history
* add proper tests for custom values

* remove try operators

* use sustrate compat for import of hash

* add license and hex

* add script to artifacts.sh

* custom values with ids not in metadata can be accessed in static interface

* fmt and clippy

* access bytes of custom values directly, even if type id wrong

* final fixes

* removing substrate-compat flag from ui tests

* Update subxt/src/custom_values/custom_values_client.rs

Co-authored-by: James Wilson <james@jsdw.me>

* remove types access in type generator

* 2 extra lines

---------

Co-authored-by: James Wilson <james@jsdw.me>
  • Loading branch information
tadeohepperle and jsdw authored Sep 12, 2023
1 parent 022d709 commit c8462de
Show file tree
Hide file tree
Showing 22 changed files with 208 additions and 44 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"testing/test-runtime",
"testing/integration-tests",
"testing/ui-tests",
"testing/generate-custom-metadata",
"macro",
"metadata",
"signer",
Expand Down
Binary file added artifacts/metadata_with_custom_values.scale
Binary file not shown.
25 changes: 20 additions & 5 deletions codegen/src/api/custom_values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use heck::ToSnakeCase as _;
use subxt_metadata::{CustomValueMetadata, Metadata};

use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use quote::{quote, ToTokens};

/// Generate the custom values mod, if there are any custom values in the metadata. Else returns None.
pub fn generate_custom_values<'a>(
Expand Down Expand Up @@ -43,18 +43,33 @@ fn generate_custom_value_fn(
// names are transformed to snake case to make for good function identifiers.
let name = custom_value.name();
let fn_name = name.to_snake_case();
// Skip elements where the fn name is already occupied. E.g. if you have custom values with names "Foo" and "foo" in the metadata.
if fn_names_taken.contains(&fn_name) {
return None;
}
let fn_name_ident = format_ident!("{fn_name}");
// if the fn_name would be an invalid ident, return None:
let fn_name_ident = syn::parse_str::<syn::Ident>(&fn_name).ok()?;
fn_names_taken.insert(fn_name);

let custom_value_hash = custom_value.hash();
let return_ty = type_gen.resolve_type_path(custom_value.type_id());

// for custom values it is important to check if the type id is actually in the metadata:
let type_is_valid = custom_value
.types()
.resolve(custom_value.type_id())
.is_some();
let (return_ty, decodable) = if type_is_valid {
let return_ty = type_gen
.resolve_type_path(custom_value.type_id())
.to_token_stream();
let decodable = quote!(#crate_path::custom_values::Yes);
(return_ty, decodable)
} else {
// if type registry does not contain the type, we can just return the Encoded scale bytes.
(quote!(()), quote!(()))
};

Some(quote!(
pub fn #fn_name_ident() -> #crate_path::custom_values::StaticAddress<#return_ty> {
pub fn #fn_name_ident(&self) -> #crate_path::custom_values::StaticAddress<#return_ty, #decodable> {
#crate_path::custom_values::StaticAddress::new_static(#name, [#(#custom_value_hash,)*])
}
))
Expand Down
5 changes: 1 addition & 4 deletions codegen/src/types/derives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,7 @@ impl DerivesRegistry {
derives: impl IntoIterator<Item = syn::Path>,
attributes: impl IntoIterator<Item = syn::Attribute>,
) {
let type_derives = self
.specific_type_derives
.entry(ty)
.or_insert_with(Derives::new);
let type_derives = self.specific_type_derives.entry(ty).or_default();
type_derives.derives.extend(derives);
type_derives.attributes.extend(attributes);
}
Expand Down
5 changes: 5 additions & 0 deletions metadata/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,11 @@ pub struct CustomValueMetadata<'a> {
}

impl<'a> CustomValueMetadata<'a> {
/// Access the underlying type registry.
pub fn types(&self) -> &PortableRegistry {
self.types
}

/// The scale encoded value
pub fn bytes(&self) -> &'a [u8] {
self.data
Expand Down
17 changes: 12 additions & 5 deletions metadata/src/utils/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,15 +405,22 @@ pub fn get_custom_metadata_hash(custom_metadata: &CustomMetadata) -> [u8; HASH_L
}

/// 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,
cache: &mut HashMap<u32, CachedHash>,
) -> [u8; HASH_LEN] {
concat_and_hash3(
&hash(custom_value.name.as_bytes()),
&get_type_hash(custom_value.types, custom_value.type_id(), cache),
&hash(custom_value.bytes()),
)
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(), cache),
)
}
}

/// Obtain the hash for a specific storage item, or an error if it's not found.
Expand Down
4 changes: 3 additions & 1 deletion scripts/artifacts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ cargo run --bin subxt metadata --version 15 > artifacts/polkadot_metadata_full.s
cargo run --bin subxt codegen --file artifacts/polkadot_metadata_full.scale | rustfmt > testing/integration-tests/src/full_client/codegen/polkadot.rs
# generate a metadata file that only contains a few pallets that we need for our examples.
cargo run --bin subxt metadata --file artifacts/polkadot_metadata_full.scale --pallets "Balances,Staking,System,Multisig,Timestamp,ParaInherent" > artifacts/polkadot_metadata_small.scale
# generate a metadata file that only contains no pallets
# generate a metadata file that contains no pallets
cargo run --bin subxt metadata --file artifacts/polkadot_metadata_full.scale --pallets "" > artifacts/polkadot_metadata_tiny.scale
# generate a metadata file that only contains some custom metadata
cargo run --bin generate-custom-metadata > artifacts/metadata_with_custom_values.scale
20 changes: 14 additions & 6 deletions subxt/src/custom_values/custom_value_address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use crate::metadata::DecodeWithMetadata;
pub trait CustomValueAddress {
/// The type of the custom value.
type Target: DecodeWithMetadata;
/// Should be set to `Yes` for Dynamic values and static values that have a valid type.
/// Should be `()` for custom values, that have an invalid type id.
type IsDecodable;

/// the name (key) by which the custom value can be accessed in the metadata.
fn name(&self) -> &str;
Expand All @@ -22,24 +25,28 @@ pub trait CustomValueAddress {

impl CustomValueAddress for str {
type Target = DecodedValueThunk;
type IsDecodable = Yes;

fn name(&self) -> &str {
self
}
}

/// Used to signal whether a [`CustomValueAddress`] can be decoded.
pub struct Yes;

/// A static address to a custom value.
pub struct StaticAddress<R> {
pub struct StaticAddress<ReturnTy, IsDecodable> {
name: &'static str,
hash: Option<[u8; 32]>,
phantom: PhantomData<R>,
phantom: PhantomData<(ReturnTy, IsDecodable)>,
}

impl<R> StaticAddress<R> {
impl<ReturnTy, IsDecodable> StaticAddress<ReturnTy, IsDecodable> {
#[doc(hidden)]
/// Creates a new StaticAddress.
pub fn new_static(name: &'static str, hash: [u8; 32]) -> Self {
StaticAddress {
pub fn new_static(name: &'static str, hash: [u8; 32]) -> StaticAddress<ReturnTy, IsDecodable> {
StaticAddress::<ReturnTy, IsDecodable> {
name,
hash: Some(hash),
phantom: PhantomData,
Expand All @@ -56,8 +63,9 @@ impl<R> StaticAddress<R> {
}
}

impl<R: DecodeWithMetadata> CustomValueAddress for StaticAddress<R> {
impl<R: DecodeWithMetadata, Y> CustomValueAddress for StaticAddress<R, Y> {
type Target = R;
type IsDecodable = Y;

fn name(&self) -> &str {
self.name
Expand Down
22 changes: 20 additions & 2 deletions subxt/src/custom_values/custom_values_client.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::client::OfflineClientT;
use crate::custom_values::custom_value_address::CustomValueAddress;
use crate::custom_values::custom_value_address::{CustomValueAddress, Yes};
use crate::error::MetadataError;
use crate::metadata::DecodeWithMetadata;
use crate::{Config, Error};
Expand All @@ -26,7 +26,7 @@ impl<T, Client> CustomValuesClient<T, Client> {
impl<T: Config, Client: OfflineClientT<T>> CustomValuesClient<T, Client> {
/// Access a custom value by the address it is registered under. This can be just a [str] to get back a dynamic value,
/// or a static address from the generated static interface to get a value of a static type returned.
pub fn at<Address: CustomValueAddress + ?Sized>(
pub fn at<Address: CustomValueAddress<IsDecodable = Yes> + ?Sized>(
&self,
address: &Address,
) -> Result<Address::Target, Error> {
Expand All @@ -48,6 +48,24 @@ impl<T: Config, Client: OfflineClientT<T>> CustomValuesClient<T, Client> {
Ok(value)
}

/// Access the bytes of a custom value by the address it is registered under.
pub fn bytes_at<Address: CustomValueAddress + ?Sized>(
&self,
address: &Address,
) -> Result<Vec<u8>, Error> {
// 1. Validate custom value shape if hash given:
self.validate(address)?;

// 2. Return the underlying bytes:
let metadata = self.client.metadata();
let custom = metadata.custom();
let custom_value = custom
.get(address.name())
.ok_or_else(|| MetadataError::CustomValueNameNotFound(address.name().to_string()))?;

Ok(custom_value.bytes().to_vec())
}

/// Run the validation logic against some custom value address you'd like to access. Returns `Ok(())`
/// if the address is valid (or if it's not possible to check since the address has no validation hash).
/// Returns an error if the address was not valid (wrong name, type or raw bytes)
Expand Down
2 changes: 1 addition & 1 deletion subxt/src/custom_values/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
mod custom_value_address;
mod custom_values_client;

pub use custom_value_address::{CustomValueAddress, StaticAddress};
pub use custom_value_address::{CustomValueAddress, StaticAddress, Yes};
pub use custom_values_client::CustomValuesClient;
2 changes: 1 addition & 1 deletion subxt/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub use primitive_types::{H160, H256, H512};

/// Wraps an already encoded byte vector, prevents being encoded as a raw byte vector as part of
/// the transaction payload
#[derive(Clone, Debug, Eq, PartialEq, Decode)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Encoded(pub Vec<u8>);

impl codec::Encode for Encoded {
Expand Down
18 changes: 18 additions & 0 deletions testing/generate-custom-metadata/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "generate-custom-metadata"
authors.workspace = true
edition.workspace = true
version.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
documentation.workspace = true
homepage.workspace = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
subxt = { workspace = true, features = ["native"] }
scale-info = { workspace = true, features = ["bit-vec"] }
frame-metadata = { workspace = true }
codec = { package = "parity-scale-codec", workspace = true, features = ["derive", "bit-vec"] }
5 changes: 5 additions & 0 deletions testing/generate-custom-metadata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# generate-custom-metadata

A small crate with a binary that creates scale encoded metadata with custom values and writes it to stdout (as raw bytes).

It also provides dispatch error types that are used in `../ui_tests`.
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
use codec::Encode;
use frame_metadata::v15::{CustomMetadata, ExtrinsicMetadata, OuterEnums, RuntimeMetadataV15};
use frame_metadata::RuntimeMetadataPrefixed;

use scale_info::form::PortableForm;
use scale_info::TypeInfo;
use scale_info::{meta_type, IntoPortable};
use std::collections::BTreeMap;

pub mod dispatch_error;

/// Generate metadata which contains a `Foo { a: u8, b: &str }` custom value.
pub fn metadata_custom_values_foo() -> RuntimeMetadataPrefixed {
let mut registry = scale_info::Registry::new();
Expand All @@ -23,7 +26,10 @@ pub fn metadata_custom_values_foo() -> RuntimeMetadataPrefixed {
}

let foo_value_metadata: frame_metadata::v15::CustomValueMetadata<PortableForm> = {
let value = Foo { a: 0, b: "Hello" };
let value = Foo {
a: 42,
b: "Have a great day!",
};
let foo_ty = scale_info::MetaType::new::<Foo>();
let foo_ty_id = registry.register_type(&foo_ty);
frame_metadata::v15::CustomValueMetadata {
Expand All @@ -32,6 +38,13 @@ pub fn metadata_custom_values_foo() -> RuntimeMetadataPrefixed {
}
};

let invalid_type_id_metadata: frame_metadata::v15::CustomValueMetadata<PortableForm> = {
frame_metadata::v15::CustomValueMetadata {
ty: u32::MAX.into(),
value: vec![0, 1, 2, 3],
}
};

// We don't care about the extrinsic type.
let extrinsic = ExtrinsicMetadata {
version: 0,
Expand All @@ -48,7 +61,7 @@ pub fn metadata_custom_values_foo() -> RuntimeMetadataPrefixed {
let unit_ty = registry.register_type(&meta_type::<()>());

// Metadata needs to contain this DispatchError, since codegen looks for it.
registry.register_type(&meta_type::<crate::utils::dispatch_error::ArrayDispatchError>());
registry.register_type(&meta_type::<dispatch_error::ArrayDispatchError>());

let metadata = RuntimeMetadataV15 {
types: registry.into(),
Expand All @@ -68,6 +81,7 @@ pub fn metadata_custom_values_foo() -> RuntimeMetadataPrefixed {
("foo".into(), foo_value_metadata.clone()),
("12".into(), foo_value_metadata.clone()),
("&Hello".into(), foo_value_metadata),
("InvalidTypeId".into(), invalid_type_id_metadata),
]),
},
};
Expand Down
17 changes: 17 additions & 0 deletions testing/generate-custom-metadata/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.

use codec::Encode;
use std::io::{self, Write};

/// Creates some scale encoded metadata with custom values and writes it out to stdout (as raw bytes)
///
/// Can be called from the root of the project with: `cargo run --bin generate-custom-metadata > output.scale`.
fn main() -> io::Result<()> {
let metadata_prefixed = generate_custom_metadata::metadata_custom_values_foo();
let stdout = io::stdout();
let mut handle = stdout.lock();
handle.write_all(&metadata_prefixed.encode())?;
Ok(())
}
9 changes: 7 additions & 2 deletions testing/ui-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ publish = false

[dev-dependencies]
trybuild = { workspace = true }
hex = { workspace = true }
scale-info = { workspace = true, features = ["bit-vec"] }
frame-metadata ={ workspace = true }
codec = { package = "parity-scale-codec", workspace = true, features = ["derive", "bit-vec"] }
frame-metadata = { workspace = true }
codec = { package = "parity-scale-codec", workspace = true, features = [
"derive",
"bit-vec",
] }
subxt = { workspace = true, features = ["native", "jsonrpsee"] }
subxt-metadata = { workspace = true }
generate-custom-metadata = { path = "../generate-custom-metadata" }
Loading

0 comments on commit c8462de

Please sign in to comment.