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

Suggestions for storage value decoding #1457

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
6 changes: 3 additions & 3 deletions codegen/src/api/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ fn generate_storage_entry_fns(
0 => (quote!(()), quote!(())),
1 => {
let key = &keys_slice[0];
if key.hasher.hash_contains_unhashed_key() {
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;
Expand All @@ -216,7 +216,7 @@ fn generate_storage_entry_fns(
|MapEntryKey {
arg_name, hasher, ..
}| {
if hasher.hash_contains_unhashed_key() {
if hasher.ends_with_key() {
quote!( #static_storage_key::new(#arg_name.borrow()) )
} else {
quote!(())
Expand All @@ -230,7 +230,7 @@ fn generate_storage_entry_fns(
hasher,
..
}| {
if hasher.hash_contains_unhashed_key() {
if hasher.ends_with_key() {
quote!( #static_storage_key<#alias_type_path> )
} else {
quote!(())
Expand Down
18 changes: 8 additions & 10 deletions metadata/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,14 +476,14 @@ pub enum StorageHasher {
}

impl StorageHasher {
/// The hash produced by a [`StorageHasher`] has 1 or 2 of the following components:
/// 1. A fixed size hash. (not present for [`StorageHasher::Identity`])
/// 2. The key that was used as an input to the hasher. (only present for
/// The hash produced by a [`StorageHasher`] can have these two components, in order:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Since the functions are a part of our public API here, I tried to give them slightly nicer names

///
/// 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 hash bytes that must be skipped to get to the 2. component, the unhashed key.
/// To check whether or not an unhashed key is contained, see [`StorageHasher::hash_bytes_before_unhashed_key`].
pub fn hash_bytes_before_unhashed_key(&self) -> usize {
/// 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,
Expand All @@ -495,10 +495,8 @@ impl StorageHasher {
}
}

/// Returns if the key a hash was produces for, is contained in the hash itself.
/// If this is `true`, [`StorageHasher::hash_bytes_before_unhashed_key`] can be used,
/// to find the offset of the start byte of the key from the start of the hash bytes.
pub fn hash_contains_unhashed_key(&self) -> bool {
/// Returns true if the key used to produce the hash is appended to the hash itself.
pub fn ends_with_key(&self) -> bool {
matches!(
self,
StorageHasher::Blake2_128Concat | StorageHasher::Twox64Concat | StorageHasher::Identity
Expand Down
4 changes: 1 addition & 3 deletions subxt/src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,10 @@ pub enum StorageAddressError {
#[error("Storage address bytes are not the expected format. Addresses need to be at least 16 bytes (pallet ++ entry) and follow a structure given by the hashers defined in the metadata.")]
UnexpectedAddressBytes,
/// An invalid hasher was used to reconstruct a value from a chunk of bytes that is part of a storage address. Hashers where the hash does not contain the original value are invalid for this purpose.
#[error("An invalid hasher was used to reconstruct a value of type {ty_name} (id={ty_id}) from a hash formed by a {hasher:?} hasher. This is only possible for concat-style hashers or the identity hasher")]
#[error("An invalid hasher was used to reconstruct a value with type ID {ty_id} from a hash formed by a {hasher:?} hasher. This is only possible for concat-style hashers or the identity hasher")]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I removed the type_name, really jsut to simplify the code a touch since in most other places we only return the type ID anyway for this sort of error.

HasherCannotReconstructKey {
/// Type id of the key's type.
ty_id: u32,
/// Type name of the key's type.
ty_name: String,
/// The invalid hasher that caused this error.
hasher: StorageHasher,
},
Expand Down
3 changes: 1 addition & 2 deletions subxt/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ mod storage_address;
mod storage_client;
mod storage_key;
mod storage_type;

pub mod utils;
mod utils;

pub use storage_client::StorageClient;

Expand Down
195 changes: 106 additions & 89 deletions subxt/src/storage/storage_key.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use super::utils::strip_storage_hash_bytes;
use crate::{
dynamic::DecodedValueThunk,
error::{Error, StorageAddressError},
metadata::{DecodeWithMetadata, Metadata},
metadata::Metadata,
utils::{Encoded, Static},
};
use scale_decode::{visitor::IgnoreVisitor, DecodeAsType};
use scale_encode::EncodeAsType;
use scale_info::PortableRegistry;
use scale_value::Value;
use subxt_metadata::StorageHasher;

use derivative::Derivative;
Expand All @@ -21,25 +22,29 @@ pub trait StorageKey {
// But that plays poorly with the `Flatten` and `Chain` structs.
fn keys_len(&self) -> usize;

/// Attempts to decode the StorageKey from a storage address that has been stripped of its root bytes.
/// Attempts to decode the StorageKey given some bytes and a set of hashers and type IDs that they are meant to represent.
///
/// Example: Imagine The `StorageKey` is a tuple (A,B) and the hashers are: [Blake2_128Concat, Twox64Concat].
/// Example: Imagine The `StorageKey` is a tuple `(A,B)` and the hashers are `[Blake2_128Concat, Twox64Concat]`.
/// Then the memory layout of the storage address is:
///
/// ```txt
/// | 8 bytes pallet hash | 8 bytes entry hash | 16 bytes hash of A | ... bytes of A | 8 bytes hash of B | ... bytes of B |
/// | 16 byte hash of A | n bytes for SCALE encoded A | 8 byte hash of B | n bytes for SCALE encoded B |
/// ```
/// `cursor` should point into a region after those first 16 bytes, at the start of a new hash.
/// `hashers_and_ty_ids` should consume all the hashers that have been used for decoding, such that there are less hashers coming to the next key.
///
/// Implementations of this must advance the `bytes` and `hashers_and_ty_ids` cursors to consume any that they are using, or
/// return an error if they cannot appropriately do so. When a tuple of such implementations is given, each implementation
/// in the tuple receives the remaining un-consumed bytes and hashers from the previous ones.
fn decode_from_bytes(
cursor: &mut &[u8],
bytes: &mut &[u8],
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

both this and the below are "cursors", so I renamed this one

hashers_and_ty_ids: &mut &[(StorageHasher, u32)],
metadata: &Metadata,
) -> Result<Self, Error>
where
Self: Sized + 'static;
}

/// Implement `StorageKey` for `()` which can be used for keyless storage entries.
/// Implement `StorageKey` for `()` which can be used for keyless storage entries,
/// or to otherwise just ignore some entry.
impl StorageKey for () {
fn keys_iter(&self) -> impl Iterator<Item = &dyn EncodeAsType> {
std::iter::empty()
Expand All @@ -50,18 +55,20 @@ impl StorageKey for () {
}

fn decode_from_bytes(
_cursor: &mut &[u8],
bytes: &mut &[u8],
hashers_and_ty_ids: &mut &[(StorageHasher, u32)],
_metadata: &Metadata,
metadata: &Metadata,
) -> Result<Self, Error> {
if hashers_and_ty_ids.is_empty() {
return Err(StorageAddressError::WrongNumberOfHashers {
hashers: 0,
fields: 1,
}
.into());
}
*hashers_and_ty_ids = &hashers_and_ty_ids[1..]; // Advance cursor by 1
// If no hashers, we just do nothing.
let Some((hasher, ty_id)) = hashers_and_ty_ids.first() else {
return Ok(());
};

// Consume the hash bytes (we don't care about the key output here).
consume_hash_returning_key_bytes(bytes, *hasher, *ty_id, metadata.types())?;
// Advance our hasher cursor as well now that we've used it.
*hashers_and_ty_ids = &hashers_and_ty_ids[1..];

Ok(())
}
}
Expand All @@ -71,8 +78,8 @@ impl StorageKey for () {
#[derive(Derivative)]
#[derivative(Clone(bound = ""), Debug(bound = ""))]
pub struct StaticStorageKey<K: ?Sized> {
pub(super) bytes: Static<Encoded>,
pub(super) _marker: std::marker::PhantomData<K>,
bytes: Static<Encoded>,
_marker: std::marker::PhantomData<K>,
}

impl<K: codec::Encode + ?Sized> StaticStorageKey<K> {
Expand Down Expand Up @@ -111,7 +118,7 @@ impl<K: ?Sized> StorageKey for StaticStorageKey<K> {
}

fn decode_from_bytes(
cursor: &mut &[u8],
bytes: &mut &[u8],
hashers_and_ty_ids: &mut &[(StorageHasher, u32)],
metadata: &Metadata,
) -> Result<Self, Error>
Expand All @@ -125,53 +132,28 @@ impl<K: ?Sized> StorageKey for StaticStorageKey<K> {
}
.into());
};
*hashers_and_ty_ids = &hashers_and_ty_ids[1..]; // Advance cursor by 1
decode_storage_key_from_hash(cursor, hasher, *ty_id, metadata)
}
}

pub fn decode_storage_key_from_hash<K: ?Sized>(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I removed this specific-to-StaticStorageKey function and made a slightly more general consume_hash_returning_bytes one that could then be used in a couple of other places too, to cut down the number of util functions about

cursor: &mut &[u8],
hasher: &StorageHasher,
ty_id: u32,
metadata: &Metadata,
) -> Result<StaticStorageKey<K>, Error> {
strip_storage_hash_bytes(cursor, hasher)?;

let bytes = *cursor;
if let Err(err) = scale_decode::visitor::decode_with_visitor(
cursor,
ty_id,
metadata.types(),
scale_decode::visitor::IgnoreVisitor,
) {
return Err(scale_decode::Error::from(err).into());
// Advance the bytes cursor, returning any key bytes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

With the consume_hash_returning_bytes function I thought it would be cleaner then to inline the other code for this impl

let key_bytes = consume_hash_returning_key_bytes(bytes, *hasher, *ty_id, metadata.types())?;
// Advance the hasher cursor now we've used it.
*hashers_and_ty_ids = &hashers_and_ty_ids[1..];

// if the hasher had no key appended, we can't decode it into a `StaticStorageKey`.
let Some(key_bytes) = key_bytes else {
return Err(StorageAddressError::HasherCannotReconstructKey {
ty_id: *ty_id,
hasher: *hasher,
}
.into());
};

// Return the key bytes.
let key = StaticStorageKey {
bytes: Static(Encoded(key_bytes.to_vec())),
_marker: std::marker::PhantomData::<K>,
};
Ok(key)
}
let bytes_decoded = bytes.len() - cursor.len();

// Note: This validation check makes sure, only zero-sized types can be decoded from
// hashers that do not support reconstruction of a value
if !hasher.hash_contains_unhashed_key() && bytes_decoded > 0 {
let ty_name = metadata
.types()
.resolve(ty_id)
.expect("ty_id is in metadata, because decode_with_visitor did not fail above; qed")
.path
.to_string();
return Err(StorageAddressError::HasherCannotReconstructKey {
ty_id,
ty_name,
hasher: *hasher,
}
.into());
};

let key_bytes = bytes[..bytes_decoded].to_vec();
let key = StaticStorageKey {
bytes: Static(Encoded(key_bytes)),
_marker: std::marker::PhantomData::<K>,
};
Ok(key)
}

impl StorageKey for Vec<scale_value::Value> {
Expand All @@ -186,32 +168,67 @@ impl StorageKey for Vec<scale_value::Value> {
}

fn decode_from_bytes(
cursor: &mut &[u8],
bytes: &mut &[u8],
hashers_and_ty_ids: &mut &[(StorageHasher, u32)],
metadata: &Metadata,
) -> Result<Self, Error>
where
Self: Sized + 'static,
{
let mut hashers_and_ty_ids_iter = hashers_and_ty_ids.iter();
let mut result: Vec<scale_value::Value> = vec![];
let mut n = 0;
while !cursor.is_empty() {
let Some((hasher, ty_id)) = hashers_and_ty_ids_iter.next() else {
// Still bytes left, but no hashers and type ids anymore to pull from: this is an unexpected error.
return Err(StorageAddressError::UnexpectedAddressBytes.into());
};
strip_storage_hash_bytes(cursor, hasher)?;
let decoded = DecodedValueThunk::decode_with_metadata(cursor, *ty_id, metadata)?;
let value = decoded.to_value()?;
result.push(value.remove_context());
n += 1;
for (hasher, ty_id) in hashers_and_ty_ids.iter() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I had a look and DecodedValueThunk::decode_with_metadata looks like it consumes all input bytes (not sure why tbh!), so the value decoding would have failed when more than one hasher I think.

I just decoded directly into a Value and figured it may as well go into a for loop where we check for remaining bytes at the end.

match consume_hash_returning_key_bytes(bytes, *hasher, *ty_id, metadata.types())? {
Some(value_bytes) => {
let value =
Value::decode_as_type(&mut &*value_bytes, *ty_id, metadata.types())?;
result.push(value.remove_context());
}
None => {
result.push(Value::unnamed_composite([]));
}
}
*hashers_and_ty_ids = &hashers_and_ty_ids[1..]; // Advance by 1 each time.
}

// We've consumed all of the hashers, so we expect to also consume all of the bytes:
if !bytes.is_empty() {
return Err(StorageAddressError::UnexpectedAddressBytes.into());
}
*hashers_and_ty_ids = &hashers_and_ty_ids[n..]; // Advance cursor by n

Ok(result)
}
}

// Skip over the hash bytes (including any key at the end), returning bytes
// representing the key if one exists, or None if the hasher has no key appended.
fn consume_hash_returning_key_bytes<'a>(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This just captures a thing we want to do in a few places, which is to skip over any bytes a StorageHasher represents, and then if there are bytes for the key, then return them.

Using this means you don't have to worry about consuming the right number of bytes anywhere else.

bytes: &mut &'a [u8],
hasher: StorageHasher,
ty_id: u32,
types: &PortableRegistry,
) -> Result<Option<&'a [u8]>, Error> {
// Strip the bytes off for the actual hash, consuming them.
let bytes_to_strip = hasher.len_excluding_key();
if bytes.len() < bytes_to_strip {
return Err(StorageAddressError::UnexpectedAddressBytes.into());
}
*bytes = &bytes[bytes_to_strip..];

// Now, find the bytes representing the key, consuming them.
let before_key = *bytes;
if hasher.ends_with_key() {
scale_decode::visitor::decode_with_visitor(bytes, ty_id, types, IgnoreVisitor)
.map_err(|err| Error::Decode(err.into()))?;

// Return the key bytes, having advanced the input cursor past them.
let key_bytes = &before_key[before_key.len() - bytes.len()..];
Ok(Some(key_bytes))
} else {
// There are no key bytes, so return None.
Ok(None)
}
}

/// Generates StorageKey implementations for tuples, e.g.
/// ```rs,norun
/// impl<A: EncodeAsType, B: EncodeAsType> StorageKey for (StorageKey<A>, StorageKey<B>) {
Expand Down Expand Up @@ -278,11 +295,11 @@ macro_rules! impl_tuples {

#[rustfmt::skip]
const _: () = {
impl_tuples!(A iter_a 0, B iter_ab 1);
impl_tuples!(A iter_a 0, B iter_ab 1, C iter_c 2);
impl_tuples!(A iter_a 0, B iter_ab 1, C iter_c 2, D iter_d 3);
impl_tuples!(A iter_a 0, B iter_ab 1, C iter_c 2, D iter_d 3, E iter_e 4);
impl_tuples!(A iter_a 0, B iter_ab 1, C iter_c 2, D iter_d 3, E iter_e 4, F iter_f 5);
impl_tuples!(A iter_a 0, B iter_ab 1, C iter_c 2, D iter_d 3, E iter_e 4, F iter_f 5, G iter_g 6);
impl_tuples!(A iter_a 0, B iter_ab 1, C iter_c 2, D iter_d 3, E iter_e 4, F iter_f 5, G iter_g 6, H iter_h 7);
impl_tuples!(A iter_a 0, B iter_b 1);
impl_tuples!(A iter_a 0, B iter_b 1, C iter_c 2);
impl_tuples!(A iter_a 0, B iter_b 1, C iter_c 2, D iter_d 3);
impl_tuples!(A iter_a 0, B iter_b 1, C iter_c 2, D iter_d 3, E iter_e 4);
impl_tuples!(A iter_a 0, B iter_b 1, C iter_c 2, D iter_d 3, E iter_e 4, F iter_f 5);
impl_tuples!(A iter_a 0, B iter_b 1, C iter_c 2, D iter_d 3, E iter_e 4, F iter_f 5, G iter_g 6);
impl_tuples!(A iter_a 0, B iter_b 1, C iter_c 2, D iter_d 3, E iter_e 4, F iter_f 5, G iter_g 6, H iter_h 7);
};
14 changes: 12 additions & 2 deletions subxt/src/storage/storage_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// see LICENSE for license details.

use super::storage_address::{StorageAddress, Yes};
use super::utils::strip_storage_addess_root_bytes;
use super::StorageKey;

use crate::{
Expand Down Expand Up @@ -311,7 +310,8 @@ where
}
}

pub(crate) fn storage_hasher_type_id_pairs(
/// Return a vec of hashers and the associated type IDs for the keys that are hashed.
fn storage_hasher_type_id_pairs(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

making it not pub didn't seem to break anything offhand :D

entry: &StorageEntryType,
metadata: &Metadata,
) -> Result<Vec<(StorageHasher, u32)>, Error> {
Expand Down Expand Up @@ -357,6 +357,16 @@ pub(crate) fn storage_hasher_type_id_pairs(
}
}

/// Strips the first 16 bytes (8 for the pallet hash, 8 for the entry hash) off some storage address bytes.
fn strip_storage_addess_root_bytes(address_bytes: &mut &[u8]) -> Result<(), StorageAddressError> {
if address_bytes.len() >= 16 {
*address_bytes = &address_bytes[16..];
Ok(())
} else {
Err(StorageAddressError::UnexpectedAddressBytes)
}
}

/// A pair of keys and values together with all the bytes that make up the storage address.
/// `keys` is `None` if non-concat hashers are used. In this case the keys could not be extracted back from the key_bytes.
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
Expand Down
Loading