Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

BoundedVec + Shims for Append/DecodeLength #8556

Merged
20 commits merged into from
Apr 16, 2021
Merged
Show file tree
Hide file tree
Changes from 9 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
2 changes: 1 addition & 1 deletion frame/support/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub use self::hash::{
};
pub use self::storage::{
StorageValue, StorageMap, StorageDoubleMap, StoragePrefixedMap, IterableStorageMap,
IterableStorageDoubleMap, migration
IterableStorageDoubleMap, migration, BoundedVec,
};
pub use self::dispatch::{Parameter, Callable};
pub use sp_runtime::{self, ConsensusEngineId, print, traits::Printable};
Expand Down
288 changes: 284 additions & 4 deletions frame/support/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@

use sp_core::storage::ChildInfo;
use sp_std::prelude::*;
use sp_std::convert::TryFrom;
use codec::{FullCodec, FullEncode, Encode, EncodeLike, Decode};
use crate::hash::{Twox128, StorageHasher, ReversibleStorageHasher};
use crate::{
hash::{Twox128, StorageHasher, ReversibleStorageHasher},
traits::Get,
};
use sp_runtime::generic::{Digest, DigestItem};
pub use sp_runtime::TransactionOutcome;

Expand Down Expand Up @@ -811,16 +815,292 @@ mod private {

impl<T: Encode> Sealed for Vec<T> {}
impl<Hash: Encode> Sealed for Digest<Hash> {}
impl<T: Value, S: Get<u32>> Sealed for BoundedVec<T, S> {}
}

impl<T: Encode> StorageAppend<T> for Vec<T> {}
impl<T: Encode> StorageDecodeLength for Vec<T> {}

/// We abuse the fact that SCALE does not put any marker into the encoding, i.e.
/// we only encode the internal vec and we can append to this vec. We have a test that ensures
/// that if the `Digest` format ever changes, we need to remove this here.
/// We abuse the fact that SCALE does not put any marker into the encoding, i.e. we only encode the
/// internal vec and we can append to this vec. We have a test that ensures that if the `Digest`
/// format ever changes, we need to remove this here.
impl<Hash: Encode> StorageAppend<DigestItem<Hash>> for Digest<Hash> {}

/// Marker trait for types `T` that can be stored in storage as `Vec<T>`.
pub trait Value: FullCodec + Clone + sp_std::fmt::Debug + Eq + PartialEq {}
gui1117 marked this conversation as resolved.
Show resolved Hide resolved
impl<T: FullCodec + Clone + sp_std::fmt::Debug + Eq + PartialEq> Value for T {}

/// A bounded vector.
///
/// It implementations for efficient append and length decoding, as with a normal `Vec<_>`, once put
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
/// into a [`StorageValue`].
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
///
/// As the name suggests, the length of the queue is always bounded. All internal operations ensure
/// this bound is respected.
#[derive(Encode, Decode, crate::DefaultNoBound, crate::CloneNoBound, crate::RuntimeDebugNoBound)]
pub struct BoundedVec<T: Value, S: Get<u32>>(Vec<T>, sp_std::marker::PhantomData<S>);
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess that this is using a type number with Get<u32> instead of a const generic because we don't want to raise the min supported rust version to 1.51?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually it probably can, I just didn't think about it :D Will try

because we don't want to raise the min supported rust version to 1.51?

I am pretty sure we always require latest stable version and don't care much about older versions.

Copy link
Contributor Author

@kianenigma kianenigma Apr 12, 2021

Choose a reason for hiding this comment

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

errr -- no.

I don't think we can make this configurable to the end-user, as it is now, with const generics. See how it is done in #8580.

We can, only if we swap things like type MaxProposal: Get<_> for const MAX_PROPOSAL: _.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, ok. I think that in the future it may be worth a large-scale refactor to use actual consts and const generics instead of faking it with type parameters, but that may have to wait for Frame 3.0.

Copy link
Member

Choose a reason for hiding this comment

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

We would already have switched to associated consts if it would be beneficial. The main advantage of using the trait is that we are much more flexible in tests. Overwriting constants would be a mess. Now we can just use thread local variables to overwrite the values.

And than there are also test runtimes where the trait enables us to read variables from the storage and thus, make it really flexible.

We would loose all this flexibility when using const generics.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, tests are also an issue with regard to const. Either of you know what kind of shenanigans we'd need to override consts? if possible at all?

Copy link
Member

Choose a reason for hiding this comment

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

We would need to make tests more generic, add new traits that provide the consts we want to override and implement X times, everytime with the const value we want. You see where this is going. This would not be easy and would just require tons of boilerplate without any advantage.


// NOTE: we could also implement this as:
// impl<T: Value, S1: Get<u32>, S2: Get<u32>> PartialEq<BoundedVec<T, S2>> for BoundedVec<T, S1>
// to allow comparison of bounded vectors with different bounds.
impl<T: Value, S: Get<u32>> PartialEq for BoundedVec<T, S> {
fn eq(&self, rhs: &Self) -> bool {
self.0 == rhs.0
}
}
impl<T: Value, S: Get<u32>> Eq for BoundedVec<T, S> {}

impl<T: Value, S: Get<u32>> BoundedVec<T, S> {
/// Create `Self` from `t` without any checks.
///
/// # WARNING
///
/// Only use when you are sure you know what you are doing.
fn unchecked_from(t: Vec<T>) -> Self {
Self(t, Default::default())
}

/// Create `Self` from `t` without any checks. Logs warnings if the bound is not being
/// respected. The additional scope can be used to indicate where a potential overflow is
/// happening.
///
/// # WARNING
///
/// Only use when you are sure you know what you are doing.
pub fn force_from(t: Vec<T>, scope: Option<&'static str>) -> Self {
if t.len() > Self::bound() {
log::warn!(
target: crate::LOG_TARGET,
"length of a bounded vector in scope {} is not respected.",
scope.unwrap_or("UNKNOWN"),
);
}

Self::unchecked_from(t)
}

/// Get the bound of the type in `usize`.
pub fn bound() -> usize {
S::get() as usize
}

/// Consume self, and return the inner `Vec`. Henceforth, the `Vec<_>` can be altered in an
/// arbitrary way. At some point, if the reverse conversion is required, `TryFrom<Vec<_>>` can
/// be used.
///
/// This is useful for cases if you need access to an internal API of the inner `Vec<_>` which
/// is not provided by the wrapper `BoundedVec`.
pub fn into_inner(self) -> Vec<T> {
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
self.0
}

/// Exactly the same semantics as [`Vec::insert`], but returns an `Err` (and is a noop) if the
/// new length of the vector exceeds `S`.
///
/// # Panics
///
/// Panics if `index > len`.
pub fn try_insert(&mut self, index: usize, element: T) -> Result<(), ()> {
if self.len() < Self::bound() {
self.0.insert(index, element);
Ok(())
} else {
Err(())
}
}

/// Exactly the same semantics as [`Vec::remove`].
///
/// # Panics
///
/// Panics if `index` is out of bounds.
pub fn remove(&mut self, index: usize) {
self.0.remove(index);
}

/// Exactly the same semantics as [`Vec::swap_remove`].
///
/// # Panics
///
/// Panics if `index` is out of bounds.
pub fn swap_remove(&mut self, index: usize) {
self.0.swap_remove(index);
}

/// Exactly the same semantics as [`Vec::retain`].
pub fn retain<F: FnMut(&T) -> bool>(&mut self, f: F) {
self.0.retain(f)
}
}

impl<T: Value, S: Get<u32>> TryFrom<Vec<T>> for BoundedVec<T, S> {
type Error = ();
fn try_from(t: Vec<T>) -> Result<Self, Self::Error> {
if t.len() <= Self::bound() {
Ok(Self::unchecked_from(t))
} else {
Err(())
}
}
}

// It is okay to give a non-mutable reference of the inner vec to anyone.
impl<T: Value, S: Get<u32>> AsRef<Vec<T>> for BoundedVec<T, S> {
fn as_ref(&self) -> &Vec<T> {
&self.0
}
}

// will allow for immutable all operations of `Vec<T>` on `BoundedVec<T>`.
impl<T: Value, S: Get<u32>> sp_std::ops::Deref for BoundedVec<T, S> {
type Target = Vec<T>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<T: Value, S: Get<u32>> codec::DecodeLength for BoundedVec<T, S> {
fn len(self_encoded: &[u8]) -> Result<usize, codec::Error> {
// `BoundedVec<T, _>` stored just a `Vec<T>`, thus the length is at the beginning in
// `Compact` form, and same implementation as `Vec<T>` can be used.
<Vec<T> as codec::DecodeLength>::len(self_encoded)
}
}

impl<T: Value, S: Get<u32>> StorageDecodeLength for BoundedVec<T, S> {}

/// Storage value that is *maybe* capable of [`StorageAppend`].
pub trait TryAppendValue<T: Value, S: Get<u32>> {
fn try_append<LikeT: EncodeLike<T>>(item: LikeT) -> Result<(), ()>;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

why not creating TryStorageappend trait, and create the function fn try_append directly in StorageMap, StorageValue and StorageDoubleMap traits, with a where clause, similarly to fn append.

Also if you want those function to be accessible on storages without having to import trait in scope, then it should also be written in "frame/support/src/storage/types/value.rs"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, so far I tried to NOT add anything extra to the bast traits StorageValue and generator::StorageValue, but yeah this will enable us to not need to import the extra trait everywhere.


/// Storage map that is *maybe* capable of [`StorageAppend`].
pub trait TryAppendMap<K: FullCodec, T: Value, S: Get<u32>> {
fn try_append<LikeK: EncodeLike<K> + Clone, LikeT: EncodeLike<T>>(
key: LikeK,
item: LikeT,
) -> Result<(), ()>;
}

impl<T: Value, S: Get<u32>, StorageValueT: generator::StorageValue<BoundedVec<T, S>>>
TryAppendValue<T, S> for StorageValueT
{
fn try_append<LikeT: EncodeLike<T>>(item: LikeT) -> Result<(), ()> {
Copy link
Member

Choose a reason for hiding this comment

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

yeah, though eventually we'll probably want this to become a DispatchError to avoid a lot of .map_err(|()| Error::<T>::ItemOverflow)s.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure which variant it should be? Other or a new (general)Overflow would be okay, but the benefit of .map_err(|()| Error::<T>::ItemOverflow) is actually that we know from which pallet it is coming from.

let bound = BoundedVec::<T, S>::bound();
let current = Self::decode_len().unwrap_or_default();
if current < bound {
// NOTE: we cannot reuse the implementation for `Vec<T>` here because we never want to
// mark `BoundedVec<T, S>` as `StorageAppend`.
let key = Self::storage_value_final_key();
sp_io::storage::append(&key, item.encode());
Ok(())
} else {
Err(())
}
}
}

impl<
K: FullCodec,
T: Value,
S: Get<u32>,
StorageMapT: generator::StorageMap<K, BoundedVec<T, S>>,
> TryAppendMap<K, T, S> for StorageMapT
{
fn try_append<LikeK: EncodeLike<K> + Clone, LikeT: EncodeLike<T>>(
key: LikeK,
item: LikeT,
) -> Result<(), ()> {
let bound = BoundedVec::<T, S>::bound();
let current = Self::decode_len(key.clone()).unwrap_or_default();
if current < bound {
let key = Self::storage_map_final_key(key);
sp_io::storage::append(&key, item.encode());
Ok(())
} else {
Err(())
}
}
}

#[cfg(test)]
pub mod bounded_vec {
use super::*;
use sp_io::TestExternalities;
use sp_std::convert::TryInto;
use crate::{assert_ok, Twox128};

crate::parameter_types! {
pub const Seven: u32 = 7;
}

crate::generate_storage_alias! { Prefix, Foo => Value<BoundedVec<u32, Seven>> }
crate::generate_storage_alias! { Prefix, FooMap => Map<(u32, Twox128), BoundedVec<u32, Seven>> }

#[test]
fn decode_len_works() {
TestExternalities::default().execute_with(|| {
let bounded: BoundedVec<u32, Seven> = vec![1, 2, 3].try_into().unwrap();
Foo::put(bounded);
assert_eq!(Foo::decode_len().unwrap(), 3);
});

TestExternalities::default().execute_with(|| {
let bounded: BoundedVec<u32, Seven> = vec![1, 2, 3].try_into().unwrap();
FooMap::insert(1, bounded);
assert_eq!(FooMap::decode_len(1).unwrap(), 3);
assert!(FooMap::decode_len(0).is_none());
assert!(FooMap::decode_len(2).is_none());
});
}

#[test]
fn try_append_works() {
TestExternalities::default().execute_with(|| {
let bounded: BoundedVec<u32, Seven> = vec![1, 2, 3].try_into().unwrap();
Foo::put(bounded);
assert_ok!(Foo::try_append(4));
assert_ok!(Foo::try_append(5));
assert_ok!(Foo::try_append(6));
assert_ok!(Foo::try_append(7));
assert_eq!(Foo::decode_len().unwrap(), 7);
assert!(Foo::try_append(8).is_err());
});

TestExternalities::default().execute_with(|| {
let bounded: BoundedVec<u32, Seven> = vec![1, 2, 3].try_into().unwrap();
FooMap::insert(1, bounded);

assert_ok!(FooMap::try_append(1, 4));
assert_ok!(FooMap::try_append(1, 5));
assert_ok!(FooMap::try_append(1, 6));
assert_ok!(FooMap::try_append(1, 7));
assert_eq!(FooMap::decode_len(1).unwrap(), 7);
assert!(FooMap::try_append(1, 8).is_err());

// append to a non-existing
assert!(FooMap::get(2).is_none());
assert_ok!(FooMap::try_append(2, 4));
assert_eq!(FooMap::get(2).unwrap(), BoundedVec::<u32, Seven>::unchecked_from(vec![4]));
assert_ok!(FooMap::try_append(2, 5));
assert_eq!(
FooMap::get(2).unwrap(),
BoundedVec::<u32, Seven>::unchecked_from(vec![4, 5])
);
});
}

#[test]
fn deref_coercion_works() {
let bounded: BoundedVec<u32, Seven> = vec![1, 2, 3].try_into().unwrap();
// these methods come from deref-ed vec.
assert_eq!(bounded.len(), 3);
assert!(bounded.iter().next().is_some());
assert!(!bounded.is_empty());
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down