9

Say I have a StorageDoubleMap that stores Data structs for each user and ContextId

    #[pallet::storage]
    pub type Info<T: Config> = StorageDoubleMap<
        _,
        Blake2_128Concat,
        T::AccountId,
        Twox64Concat,
        T::ContextId,
        Data,
    >;

Is there an option to limit the amount of entries per AccountId? Or should I keep a count of entries in a separate map?

Update:

Accessing a specific ContextId for an AccountId quickly is important to me, that's why I chose StorageDoubleMap.

Moreover, a common operation will be to iterate over all ContextIds for an AccountId, which StorageDoubleMap also has support for.

3 Answers 3

7

You could also use a vector as a value to AccountId.

#[pallet::storage]
#[pallet::getter(fn addresses_owned)]
/// Keeps track of address owners.
pub(super) type AddressOwners<T: Config> =
    StorageMap<_, Twox64Concat, Address, BoundedVec<T::AccountId, T::MaxAddressOwners>, ValueQuery>;

You can find the rest of the code here.

Notice that in this case T::MaxAddressOwners is a runtime constant. Is should be specified in the pallet config section. The example is described here.

To add a value to the list you can use the code below:

#[pallet::weight(10_000)]
pub fn add_owner(
    origin: OriginFor<T>,
    address: Vec<u8>,
    owner: Option<T::AccountId>,
    ) -> DispatchResult {

    let account = ensure_signed(origin)?;
    let new_owner = match owner {
        Some(res) => res,
        None => account.clone()
    };

    let has_owners = Self::has_owners(&address);
    if has_owners {
        ensure!(Self::is_owned(&new_owner, &address) == false, <Error<T>>::AccountInOwners);
        ensure!(Self::is_owned(&account, &address), <Error<T>>::AddressNotOwned);
    }
            

    <AddressOwners<T>>::try_mutate(&address, |owner_vec| {
        owner_vec.try_push(new_owner.clone())
    }).map_err(|_| <Error<T>>::ExceedMaxAddressOwners)?;

    Self::deposit_event(Event::AddOwner(address, new_owner));
    Ok(())
}

The origin can be found here.

2
  • Makes sense. Although, I will need to access a specific ContextId for an AccountId frequently. I suppose in your model I would have to iterate over the vector to find that specific ContextId, no?
    – Angelo
    Commented Mar 21, 2022 at 18:19
  • 1
    Yes, that's true. You will have to iterate through the values Vec. One way of doing that is used here. Commented Mar 21, 2022 at 20:24
5

Any solution to this problem, whether provided by FRAME or implemented manually, would require storing the number of items currently in the map in some other storage location, since you would never want to figure out how many items are there by iterating through all the storage items.

So you could have:

#[pallet::storage]
pub type Info<T: Config> = StorageDoubleMap<
    _,
    Blake2_128Concat,
    T::AccountId,
    Twox64Concat,
    T::ContextId,
    Data,
>;

#[pallet::storage]
pub type CountForInfo<T: Config> = StorageMap<
    _,
    Blake2_128Concat,
    T::AccountId,
    u32,
    ValueQuery,
>;

And then, before you do any insert you check this value:

fn try_insert_info(who: T::AccountId, context: T::ContextId, data: Data) -> DispatchResult {
    let already_exists = Info::<T>::contains_key(who, context);
    if !already_exists {
        let count = CountForInfo::<T>::get(who);
        if count < LIMIT {
            CountForInfo::<T>::insert(who, count + 1);
        } else {
            return Err(Error::<T>::TooManyItems.into())
        }
    }
    
    Info::<T>::insert(who, context, data);
    Ok(())
}

You will notice this function, versus a normal insert is way more expensive, since you need to check if this is a brand new storage item, or one that already exists, and if it is new, then you need to check and update the count before inserting the new storage item.

It will almost always be more efficient for you to track this yourself rather than building an abstraction over this behavior, since you will be able to avoid writing / modifying the count storage item in certain situations.

For example, imagine you do some code which removes an existing key, and then inserts a new key. In this case, you can likely avoid updating or checking the count at all, which can save you multiple lines of storage access.

The only time it makes sense to use an abstraction like this is when the code is so complex that avoiding any manual tracking errors outweighs the additional overhead, but even still, probably best to track it manually.

2
  • I understand, I was almost thinking about building such an abstraction. Makes sense what you said about saving checks for specific operations. Will take that into account. Btw, is iterating over ContextIds using iter_prefix too expensive?
    – Angelo
    Commented Mar 21, 2022 at 18:21
  • 1
    iter_prefix will cause N storage reads, and practically has no upper bound to what N would be. This is extremely expensive, and if you have an operation like this, probably you are better off using a Vec.
    – Shawn Tabrizi
    Commented Mar 21, 2022 at 18:50
4

At the moment I think you would need to keep some other storage items to track the amount of entries per AccountId.

However, I think being able to bound the number of entries per key in a double map or n map would be a useful abstraction for the frame support macros to expose - it would be worth making an issue in https://github.com/paritytech/substrate for this feature request.

1
  • Yeah, that would be neat, with standardized error handling for operations that exceed the limit. Managing it outside may lead to errors that allow to exceed it
    – Angelo
    Commented Mar 19, 2022 at 23:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.