Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typed Storage Keys #1419

Merged
merged 49 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
eeded2c
first iteration on storage multi keys
tadeohepperle Feb 8, 2024
4f052d0
decoding values from concat style hashers
tadeohepperle Feb 8, 2024
e4e4437
move util functions and remove comments
tadeohepperle Feb 8, 2024
3f20d77
change codegen for storage keys and fix examples
tadeohepperle Feb 9, 2024
3f0d8f0
Merge branch 'master' into tadeohepperle/decoding-storage-keys
tadeohepperle Feb 9, 2024
28e1b77
trait bounds don't match scale value...
tadeohepperle Feb 13, 2024
489ccdc
fix trait bounds and examples
tadeohepperle Feb 13, 2024
23203da
reconstruct storage keys in iterations
tadeohepperle Feb 13, 2024
7a36ce3
build(deps): bump js-sys from 0.3.67 to 0.3.68 (#1428)
dependabot[bot] Feb 12, 2024
d7c658d
build(deps): bump clap from 4.4.18 to 4.5.0 (#1427)
dependabot[bot] Feb 12, 2024
b21e94d
build(deps): bump either from 1.9.0 to 1.10.0 (#1425)
dependabot[bot] Feb 12, 2024
252e31e
build(deps): bump thiserror from 1.0.56 to 1.0.57 (#1424)
dependabot[bot] Feb 12, 2024
db12bd7
build(deps): bump jsonrpsee from 0.21.0 to 0.22.0 (#1426)
dependabot[bot] Feb 12, 2024
5313f6a
subxt: Derive `std::cmp` traits for subxt payloads and addresses (#1429)
lexnv Feb 13, 2024
8e1f77f
Merge
tadeohepperle Feb 13, 2024
a2ad8d1
Merge branch 'master' into tadeohepperle/decoding-storage-keys
tadeohepperle Feb 14, 2024
cef1f6b
fix clippy
tadeohepperle Feb 15, 2024
62fd01f
add integration tests
tadeohepperle Feb 15, 2024
394b206
Merge branch 'master' into tadeohepperle/decoding-storage-keys
tadeohepperle Feb 15, 2024
b131b0f
fix doc tests
tadeohepperle Feb 16, 2024
1b856bc
change hashing logic for hashers=1
tadeohepperle Feb 16, 2024
b0ddafa
refactor
tadeohepperle Feb 16, 2024
3202601
clippy and fmt
tadeohepperle Feb 19, 2024
0b4f2ea
Merge branch 'master' into tadeohepperle/decoding-storage-keys
tadeohepperle Feb 19, 2024
4c750f4
regenerate polkadot file which got changed by the automatic PR
tadeohepperle Feb 19, 2024
a404c85
nested design for storage keys
tadeohepperle Feb 23, 2024
e87eb3c
refactor codegen
tadeohepperle Feb 26, 2024
9c27b47
codegen adjustments
tadeohepperle Feb 26, 2024
585c01c
Merge branch 'master' into tadeohepperle/decoding-storage-keys
tadeohepperle Feb 26, 2024
4bfaa4c
fix storage hasher codegen test
tadeohepperle Feb 27, 2024
bd01aab
Suggestions for storage value decoding (#1457)
jsdw Feb 28, 2024
a284ed8
integrate nits from PR
tadeohepperle Feb 28, 2024
5a68e8e
add fuzztest for storage keys, fix decoding bug
tadeohepperle Feb 28, 2024
e4195b2
clippy and fmt
tadeohepperle Feb 28, 2024
97962ee
clippy
tadeohepperle Feb 29, 2024
8f0eb04
Merge branch 'master' into tadeohepperle/decoding-storage-keys
tadeohepperle Feb 29, 2024
9b33cbc
Niklas Suggestions
tadeohepperle Feb 29, 2024
b7a6b41
Merge branch 'master' into tadeohepperle/decoding-storage-keys
tadeohepperle Mar 1, 2024
aa5e703
lifetime issues and iterator impls
tadeohepperle Mar 4, 2024
1eaa75d
fmt and clippy
tadeohepperle Mar 4, 2024
56f1864
Merge branch 'master' into tadeohepperle/decoding-storage-keys
tadeohepperle Mar 4, 2024
c5e071d
regenerate polkadot.rs
tadeohepperle Mar 4, 2024
4a3b936
fix storage key encoding for empty keys
tadeohepperle Mar 5, 2024
a660a67
Merge branch 'master' into tadeohepperle/decoding-storage-keys
tadeohepperle Mar 5, 2024
f7a530c
rename trait methods for storage keys
tadeohepperle Mar 5, 2024
26947b5
fix hasher bug...
tadeohepperle Mar 5, 2024
81cd16e
impl nits, add iterator struct seperate from `StorageHashers`
tadeohepperle Mar 6, 2024
1a42c9e
clippy fix
tadeohepperle Mar 6, 2024
809331e
remove println
tadeohepperle Mar 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 124 additions & 25 deletions codegen/src/api/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use quote::{format_ident, quote};
use scale_info::TypeDef;
use scale_typegen::{typegen::type_path::TypePath, TypeGenerator};
use subxt_metadata::{
PalletMetadata, StorageEntryMetadata, StorageEntryModifier, StorageEntryType,
PalletMetadata, StorageEntryMetadata, StorageEntryModifier, StorageEntryType, StorageHasher,
};

use super::CodegenError;
Expand Down Expand Up @@ -75,43 +75,83 @@ fn generate_storage_entry_fns(
let alias_module_name = format_ident!("{snake_case_name}");
let alias_storage_path = quote!( types::#alias_module_name::#alias_name );

let storage_entry_map = |idx, id| {
let ident: Ident = format_ident!("_{}", idx);
struct MapEntryKey {
arg_name: Ident,
alias_type_def: TokenStream,
alias_type_path: TokenStream,
hasher: StorageHasher,
}

let map_entry_key = |idx, id, hasher| -> MapEntryKey {
let arg_name: Ident = format_ident!("_{}", idx);
let ty_path = type_gen
.resolve_type_path(id)
.expect("type is in metadata; qed");

let alias_name = format_ident!("Param{}", idx);
let alias_type = primitive_type_alias(&ty_path);

let alias_type = quote!( pub type #alias_name = #alias_type; );
let path_to_alias = quote!( types::#alias_module_name::#alias_name );
let alias_type_def = quote!( pub type #alias_name = #alias_type; );
let alias_type_path = quote!( types::#alias_module_name::#alias_name );

(ident, alias_type, path_to_alias)
MapEntryKey {
arg_name,
alias_type_def,
alias_type_path,
hasher,
}
};

let keys: Vec<(Ident, TokenStream, TokenStream)> = match storage_entry.entry_type() {
let keys: Vec<MapEntryKey> = match storage_entry.entry_type() {
StorageEntryType::Plain(_) => vec![],
StorageEntryType::Map { key_ty, .. } => {
StorageEntryType::Map {
key_ty, hashers, ..
} => {
match &type_gen
.resolve_type(*key_ty)
.expect("key type should be present")
.type_def
{
// An N-map; return each of the keys separately.
TypeDef::Tuple(tuple) => tuple
.fields
.iter()
.enumerate()
.map(|(idx, f)| storage_entry_map(idx, f.id))
.collect::<Vec<_>>(),
TypeDef::Tuple(tuple) => {
let key_count = tuple.fields.len();
Copy link
Member

@niklasad1 niklasad1 Feb 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key_count and hasher_count can't be 0 right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in reality, this is unlikely, but if they were, we just return vec![] below.

let hasher_count = hashers.len();
if hasher_count != 1 && hasher_count != key_count {
return Err(CodegenError::InvalidStorageHasherCount {
storage_entry_name: storage_entry.name().to_owned(),
key_count,
hasher_count,
});
}

let mut map_entry_keys: Vec<MapEntryKey> = vec![];
for (idx, field) in tuple.fields.iter().enumerate() {
// Note: these are in bounds because of the checks above, qed;
let hasher = if idx >= hasher_count {
hashers[0]
} else {
hashers[idx]
};
map_entry_keys.push(map_entry_key(idx, field.id, hasher));
}
map_entry_keys
}
// A map with a single key; return the single key.
_ => {
vec![storage_entry_map(0, *key_ty)]
let Some(hasher) = hashers.first() else {
return Err(CodegenError::InvalidStorageHasherCount {
storage_entry_name: storage_entry.name().to_owned(),
key_count: 1,
hasher_count: 0,
});
};

vec![map_entry_key(0, *key_ty, *hasher)]
}
}
}
};

let pallet_name = pallet.name();
let storage_name = storage_entry.name();
let Some(storage_hash) = pallet.storage_hash(storage_name) else {
Expand All @@ -133,6 +173,10 @@ fn generate_storage_entry_fns(
StorageEntryModifier::Optional => quote!(()),
};

// Note: putting `#crate_path::storage::address::StaticStorageKey` into this variable is necessary
// to get the line width below a certain limit. If not done, rustfmt will refuse to format the following big expression.
// for more information see [this post](https://users.rust-lang.org/t/rustfmt-silently-fails-to-work/75485/4).
let static_storage_key: TokenStream = quote!(#crate_path::storage::address::StaticStorageKey);
let all_fns = (0..=keys.len()).map(|n_keys| {
let keys_slice = &keys[..n_keys];
let (fn_name, is_fetchable, is_iterable) = if n_keys == keys.len() {
Expand All @@ -146,20 +190,73 @@ fn generate_storage_entry_fns(
};
(fn_name, false, true)
};
let is_fetchable_type = is_fetchable.then_some(quote!(#crate_path::storage::address::Yes)).unwrap_or(quote!(()));
let is_iterable_type = is_iterable.then_some(quote!(#crate_path::storage::address::Yes)).unwrap_or(quote!(()));
let key_impls = keys_slice.iter().map(|(field_name, _, _)| quote!( #crate_path::storage::address::make_static_storage_map_key(#field_name.borrow()) ));
let key_args = keys_slice.iter().map(|(field_name, _, path_to_alias )| {
quote!( #field_name: impl ::std::borrow::Borrow<#path_to_alias> )
});
let is_fetchable_type = is_fetchable
.then_some(quote!(#crate_path::storage::address::Yes))
.unwrap_or(quote!(()));
let is_iterable_type = is_iterable
.then_some(quote!(#crate_path::storage::address::Yes))
.unwrap_or(quote!(()));

let (keys, keys_type) = match keys_slice.len() {
0 => (quote!(()), quote!(())),
1 => {
let key = &keys_slice[0];
if key.hasher.ends_with_key() {
let arg = &key.arg_name;
let keys = quote!(#static_storage_key::new(#arg.borrow()));
let path = &key.alias_type_path;
let path = quote!(#static_storage_key<#path>);
(keys, path)
} else {
(quote!(()), quote!(()))
}
}
_ => {
let keys_iter = keys_slice.iter().map(
|MapEntryKey {
arg_name, hasher, ..
}| {
if hasher.ends_with_key() {
quote!( #static_storage_key::new(#arg_name.borrow()) )
} else {
quote!(())
}
},
);
let keys = quote!( (#(#keys_iter,)*) );
let paths_iter = keys_slice.iter().map(
|MapEntryKey {
alias_type_path,
hasher,
..
}| {
if hasher.ends_with_key() {
quote!( #static_storage_key<#alias_type_path> )
} else {
quote!(())
}
},
);
let paths = quote!( (#(#paths_iter,)*) );
(keys, paths)
}
};

let key_args = keys_slice.iter().map(
|MapEntryKey {
arg_name,
alias_type_path,
..
}| quote!( #arg_name: impl ::std::borrow::Borrow<#alias_type_path> ),
);

quote!(
#docs
pub fn #fn_name(
&self,
#(#key_args,)*
) -> #crate_path::storage::address::Address::<
#crate_path::storage::address::StaticStorageMapKey,
#keys_type,
#alias_storage_path,
#is_fetchable_type,
#is_defaultable_type,
Expand All @@ -168,14 +265,16 @@ fn generate_storage_entry_fns(
#crate_path::storage::address::Address::new_static(
#pallet_name,
#storage_name,
vec![#(#key_impls,)*],
#keys,
[#(#storage_hash,)*]
)
}
)
});

let alias_types = keys.iter().map(|(_, alias_type, _)| alias_type);
let alias_types = keys
.iter()
.map(|MapEntryKey { alias_type_def, .. }| alias_type_def);

let types_mod_ident = type_gen.types_mod_ident();
// Generate type alias for the return type only, since
Expand Down Expand Up @@ -231,7 +330,7 @@ mod tests {
name,
modifier: v15::StorageEntryModifier::Optional,
ty: v15::StorageEntryType::Map {
hashers: vec![],
hashers: vec![v15::StorageHasher::Blake2_128Concat],
key,
value: meta_type::<bool>(),
},
Expand Down
18 changes: 12 additions & 6 deletions codegen/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,21 @@ pub enum CodegenError {
#[error("Call variant for type {0} must have all named fields. Make sure you are providing a valid substrate-based metadata")]
InvalidCallVariant(u32),
/// Type should be an variant/enum.
#[error(
"{0} type should be an variant/enum type. Make sure you are providing a valid substrate-based metadata"
)]
#[error("{0} type should be an variant/enum type. Make sure you are providing a valid substrate-based metadata")]
InvalidType(String),
/// Extrinsic call type could not be found.
#[error(
"Extrinsic call type could not be found. Make sure you are providing a valid substrate-based metadata"
)]
#[error("Extrinsic call type could not be found. Make sure you are providing a valid substrate-based metadata")]
MissingCallType,
/// There are too many or too few hashers.
#[error("Could not generate functions for storage entry {storage_entry_name}. There are {key_count} keys, but only {hasher_count} hashers. The number of hashers must equal the number of keys or be exactly 1.")]
InvalidStorageHasherCount {
/// The name of the storage entry
storage_entry_name: String,
/// Number of keys
key_count: usize,
/// Number of hashers
hasher_count: usize,
},
/// Cannot generate types.
#[error("Type Generation failed: {0}")]
TypeGeneration(#[from] TypegenError),
Expand Down
8 changes: 2 additions & 6 deletions metadata/src/from_into/v14.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,7 @@ fn generate_outer_enums(
) -> Result<v15::OuterEnums<scale_info::form::PortableForm>, TryFromError> {
let find_type = |name: &str| {
metadata.types.types.iter().find_map(|ty| {
let Some(ident) = ty.ty.path.ident() else {
return None;
};
let ident = ty.ty.path.ident()?;

if ident != name {
return None;
Expand Down Expand Up @@ -368,9 +366,7 @@ fn generate_outer_error_enum_type(
.pallets
.iter()
.filter_map(|pallet| {
let Some(error) = &pallet.error else {
return None;
};
let error = pallet.error.as_ref()?;

// Note: using the `alloc::format!` macro like in `let path = format!("{}Error", pallet.name);`
// leads to linker errors about extern function `_Unwind_Resume` not being defined.
Expand Down
29 changes: 29 additions & 0 deletions metadata/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,35 @@ pub enum StorageHasher {
Identity,
}

impl StorageHasher {
/// The hash produced by a [`StorageHasher`] can have these two components, in order:
///
/// 1. A fixed size hash. (not present for [`StorageHasher::Identity`]).
/// 2. The SCALE encoded key that was used as an input to the hasher (only present for
/// [`StorageHasher::Twox64Concat`], [`StorageHasher::Blake2_128Concat`] or [`StorageHasher::Identity`]).
///
/// This function returns the number of bytes used to represent the first of these.
pub fn len_excluding_key(&self) -> usize {
match self {
StorageHasher::Blake2_128Concat => 16,
StorageHasher::Twox64Concat => 8,
StorageHasher::Blake2_128 => 16,
StorageHasher::Blake2_256 => 32,
StorageHasher::Twox128 => 16,
StorageHasher::Twox256 => 32,
StorageHasher::Identity => 0,
}
}

/// Returns true if the key used to produce the hash is appended to the hash itself.
pub fn ends_with_key(&self) -> bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this is a bit hard to hard to read, maybe we could do contains_key, contains_plaintext or is_concat_with_key... not sure

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chanegd it from contains_key or something I think! I went with ends_ just to fit in more nicely with the above method which is giving the length of the hash apart from the key bit.

contains_key would work too though for me!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either looks good to me :D

matches!(
self,
StorageHasher::Blake2_128Concat | StorageHasher::Twox64Concat | StorageHasher::Identity
)
}
}

/// Is the storage entry optional, or does it have a default value.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum StorageEntryModifier {
Expand Down
7 changes: 4 additions & 3 deletions subxt/examples/storage_iterating.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// a time from the node, but we always iterate over one at a time).
let mut results = api.storage().at_latest().await?.iter(storage_query).await?;

while let Some(Ok((key, value))) = results.next().await {
println!("Key: 0x{}", hex::encode(&key));
println!("Value: {:?}", value);
while let Some(Ok(kv)) = results.next().await {
println!("Keys decoded: {:?}", kv.keys);
println!("Key: 0x{}", hex::encode(&kv.key_bytes));
println!("Value: {:?}", kv.value);
}

Ok(())
Expand Down
11 changes: 6 additions & 5 deletions subxt/examples/storage_iterating_dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api = OnlineClient::<PolkadotConfig>::new().await?;

// Build a dynamic storage query to iterate account information.
// With a dynamic query, we can just provide an empty Vec as the keys to iterate over all entries.
let keys = Vec::<()>::new();
// With a dynamic query, we can just provide an empty vector as the keys to iterate over all entries.
let keys: Vec<scale_value::Value> = vec![];
let storage_query = subxt::dynamic::storage("System", "Account", keys);

// Use that query to return an iterator over the results.
let mut results = api.storage().at_latest().await?.iter(storage_query).await?;

while let Some(Ok((key, value))) = results.next().await {
println!("Key: 0x{}", hex::encode(&key));
println!("Value: {:?}", value.to_value()?);
while let Some(Ok(kv)) = results.next().await {
println!("Keys decoded: {:?}", kv.keys);
println!("Key: 0x{}", hex::encode(&kv.key_bytes));
println!("Value: {:?}", kv.value.to_value()?);
}

Ok(())
Expand Down
8 changes: 4 additions & 4 deletions subxt/examples/storage_iterating_partial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Get back an iterator of results.
let mut results = api.storage().at_latest().await?.iter(storage_query).await?;

while let Some(Ok((key, value))) = results.next().await {
println!("Key: 0x{}", hex::encode(&key));
println!("Value: {:?}", value);
while let Some(Ok(kv)) = results.next().await {
println!("Keys decoded: {:?}", kv.keys);
println!("Key: 0x{}", hex::encode(&kv.key_bytes));
println!("Value: {:?}", kv.value);
Comment on lines +42 to +44
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The examples look great!

}

Ok(())
}

Expand Down
2 changes: 1 addition & 1 deletion subxt/src/book/usage/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
//! // A static query capable of iterating over accounts:
//! let storage_query = polkadot::storage().system().account_iter();
//! // A dynamic query to do the same:
//! let storage_query = subxt::dynamic::storage("System", "Account", Vec::<u8>::new());
//! let storage_query = subxt::dynamic::storage("System", "Account", ());
//! ```
//!
//! Some storage entries are maps with multiple keys. As an example, we might end up with
Expand Down
Loading
Loading