Using Events

An event is a mechanism for emitting notifications about specific actions or state changes that occur within a blockchain runtime. Events are typically used to inform the outside world about occurrences such as token transfers, account creations, or other significant operations within the blockchain.

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
    /// A user has successfully set a new value.
    SomethingStored {
        /// The new value set.
        something: u32,
        /// The account who set the new value.
        who: T::AccountId,
    },
}
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::do_something())]
pub fn do_something(origin: OriginFor<T>, something: u32) -> DispatchResult {
    // Check that the extrinsic was signed and get the signer.
    let who = ensure_signed(origin)?;

    // Update storage.
    Something::<T>::put(something);

    // Emit an event.
    Self::deposit_event(Event::SomethingStored { something, who });

    // Return a successful `DispatchResult`
    Ok(())
}

Quiz

Declaring a StorageMap

pallets/simple-map View on GitHub

We declare a single storage map with the following syntax:

#[pallet::storage]
#[pallet::getter(fn simple_map)]
pub(super) type SimpleMap<T: Config> =
    StorageMap<_, Blake2_128Concat, T::AccountId, u32, ValueQuery>;

Explanation of the code:

  • SimpleMap - the name of the storage map

  • #[pallet::getter(fn simple_map)] - a getter function simple_map is created using pallet getter macros.

  • Blake2_128Concat - its an hasher used in map. More on this below.

Map contains key and its value:

  • T::AccountId - its the data type of key of the map.

  • u64 - its the data type of value of the map.

  • ValueQuery - If you omit ValueQuery, when interacting with a simple map, you will get an Option<u32>, which means that if you try to get a value from your StorageMap, you will get either Some(value) or None. Using ValueQuery will always return a value, so you don't have to deal with unwrapping the get calls.

Choosing a Hasher

Hasher to use to hash keys to insert to storage.

Although the syntax above is complex, most of it should be straightforward if you've understood the recipe on storage values. The last unfamiliar piece of writing a storage map is choosing which hasher to use. In general you should choose one of the three following hashers. The choice of hasher will affect the performance and security of your chain. If you don't want to think much about this, just choose Blake2_128Concat and skip to the next section.

Blake2_128Concat

This is a cryptographically secure hash function, and is always safe to use. It is reasonably efficient, and will keep your storage tree balanced. You must choose this hasher if users of your chain have the ability to affect the storage keys. In this pallet, the keys are AccountIds. At first it may seem that the user doesn't affect the AccountId, but in reality a malicious user can generate thousands of accounts and use the one that will affect the chain's storage tree in the way the attacker likes. For this reason, we have chosen to use the Blake2_128Concat hasher.

Twox64Concat

This hasher is not cryptographically secure, but is more efficient than blake2. Thus it represents trading security for performance. You should not use this hasher if chain users can affect the storage keys. However, it is perfectly safe to use this hasher to gain performance in scenarios where the users do not control the keys. For example, if the keys in your map are sequentially increasing indices and users cannot cause the indices to rapidly increase, then this is a perfectly reasonable choice.

Identity

The Identity "hasher" is really not a hasher at all, but merely an identity function that returns the same value it receives. This hasher is only an option when the key type in your storage map is already a hash, and is not controllable by the user. If you're in doubt whether the user can influence the key just use blake2.

The Storage Map API

Documentation This pallet demonstrated some of the most common methods available in a storage map including insert, get, take, and contains_key.

// Insert
<SimpleMap<T>>::insert(&user, entry);

// Get
let entry = <SimpleMap<T>>::get(account);

// Take
let entry = <SimpleMap<T>>::take(&user);

// Contains Key
<SimpleMap<T>>::contains_key(&user)

// Mutate
<SimpleMap<T>>::mutate(&user, |entry_option| {
							 *entry_option = Some(entry);
            });

insert and mutate

When deciding between mutate and insert to update storage, consider the following:

Insert performs a simple write operation to the database, which is the more efficient option.

On the other hand, mutate involves a read operation followed by a write, making it a more expensive database operation.

Therefore, when you have the option to use insert (i.e., you don't need to read the existing value), it's recommended to use insert over mutate.

Insert is suitable for inserting or overwriting an existing value. If you simply want to store a specific value, insert is the way to go.

Mutate, however, is designed for scenarios where you need to modify the existing value or make decisions based on its current state. Use mutate when you need to perform conditional updates or modifications that depend on the current value."

Quiz

Dev Mode

Dev mode allows you to write code without assigning weights to functions. Weights are an essential mechanism for measuring and limiting usage, establishing an economic incentive structure, preventing network overload, and mitigating DoS vulnerabilities. Weights are calculated during benchmarking.

If you want to write functions without doing benchmarking, you can use dev mode. You can write the benchmark later on, once you've completed the prototyping and testing.

To convert your pallet to dev mode, use #[frame_support::pallet(dev_mode)]

Use:

#[frame_support::pallet(dev_mode)]
pub mod pallet {

instead of

#[frame_support::pallet]
pub mod pallet {

You can write functions without assigning any weight using #[pallet::weight(0)]

#[pallet::call]
impl<T: Config> Pallet<T> {
	#[pallet::call_index(0)]
	#[pallet::weight(0)]
	pub fn do_something(origin: OriginFor<T>, something: u32) -> DispatchResult {
		let who = ensure_signed(origin)?;

		Something::<T>::put(something);
		Self::deposit_event(Event::SomethingStored { something, who });

		Ok(())
	}

Quiz

Cache Multiple Calls

pallets/storage-cache View on GitHub

Calls to runtime storage have an associated cost and developers should strive to minimize the number of calls.

#[pallet::storage]
#[pallet::getter(fn some_copy_value)]
pub(super) type SomeCopyValue<T: Config> = StorageValue<_, u32>;

#[pallet::storage]
#[pallet::getter(fn king_member)]
pub(super) type KingMember<T: Config> = StorageValue<_, T::AccountId>;

#[pallet::storage]
#[pallet::getter(fn group_members)]
pub(super) type GroupMembers<T: Config> = StorageValue<_, Vec<T::AccountId>>;

Copy Types

For Copy types, it is easy to reuse previous storage calls by simply reusing the value, which is automatically cloned upon reuse. In the code below, the second call is unnecessary:

pub fn increase_value_no_cache(
    origin: OriginFor<T>,
    some_val: u32,
) -> DispatchResultWithPostInfo {
    let _ = ensure_signed(origin)?;
    let original_call = <SomeCopyValue<T>>::get();
    let some_calculation = original_call
        .unwrap()
        .checked_add(some_val)
        .ok_or("addition overflowed1")?;
    // this next storage call is unnecessary and is wasteful
    let unnecessary_call = <SomeCopyValue<T>>::get();
    // should've just used `original_call` here because u32 is copy
    let another_calculation = some_calculation
        .checked_add(unnecessary_call.unwrap())
        .ok_or("addition overflowed2")?;
    <SomeCopyValue<T>>::put(another_calculation);
    let now = <frame_system::Pallet<T>>::block_number();
    Self::deposit_event(Event::InefficientValueChange(another_calculation, now));
    Ok(().into())
}

Instead, the initial call value should be reused. In this example, the SomeCopyValue value is Copy so we should prefer the following code without the unnecessary second call to storage:

pub fn increase_value_w_copy(
    origin: OriginFor<T>,
    some_val: u32,
) -> DispatchResultWithPostInfo {
    let _ = ensure_signed(origin)?;
    let original_call = <SomeCopyValue<T>>::get();
    let some_calculation = original_call
        .unwrap()
        .checked_add(some_val)
        .ok_or("addition overflowed1")?;
    // uses the original_call because u32 is copy
    let another_calculation = some_calculation
        .checked_add(original_call.unwrap())
        .ok_or("addition overflowed2")?;
    <SomeCopyValue<T>>::put(another_calculation);
    let now = <frame_system::Pallet<T>>::block_number();
    Self::deposit_event(Event::BetterValueChange(another_calculation, now));
    Ok(().into())
}

Clone Types

If the type was not Copy, but was Clone, then it is still better to clone the value in the method than to make another call to runtime storage.

The runtime methods enable the calling account to swap the T::AccountId value in storage if

  1. the existing storage value is not in GroupMembers AND
  2. the calling account is in GroupMembers

The first implementation makes a second unnecessary call to runtime storage instead of cloning the call for existing_key:

pub fn swap_king_no_cache(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
    let new_king = ensure_signed(origin)?;
    let existing_king = <KingMember<T>>::get();

    // only places a new account if
    // (1) the existing account is not a member &&
    // (2) the new account is a member
    ensure!(
        !Self::is_member(&existing_king.unwrap()),
        "current king is a member so maintains priority"
    );
    ensure!(
        Self::is_member(&new_king),
        "new king is not a member so doesn't get priority"
    );

    // BAD (unnecessary) storage call
    let old_king = <KingMember<T>>::get();
    // place new king
    <KingMember<T>>::put(new_king.clone());

    Self::deposit_event(Event::InefficientKingSwap(old_king.unwrap(), new_king));
    Ok(().into())
}

If the existing_key is used without a clone in the event emission instead of old_king, then the compiler returns the following error:

error[E0382]: use of moved value: `new_king`
   --> pallets/storage-cache/src/lib.rs:190:79
    |
168 |             let new_king = ensure_signed(origin)?;
    |                 -------- move occurs because `new_king` has type `<T as frame_system::Config>::AccountId`, which does not implement the `Copy` trait
...
188 |             <KingMember<T>>::put(new_king);
    |                                  -------- value moved here
189 |
190 |             Self::deposit_event(Event::InefficientKingSwap(old_king.unwrap(), new_king));
    |                                                                               ^^^^^^^^ value used here after move
    |
help: consider cloning the value if the performance cost is acceptable
    |
188 |             <KingMember<T>>::put(new_king.clone());
    |                                          ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `pallet-storage-cache` (lib) due to 1 previous error

Fixing this only requires cloning the original value before it is moved:

pub fn swap_king_with_cache(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
    let new_king = ensure_signed(origin)?;
    let existing_king = <KingMember<T>>::get();
    // prefer to clone previous call rather than repeat call unnecessarily
    let old_king = existing_king.clone();

    // only places a new account if
    // (1) the existing account is not a member &&
    // (2) the new account is a member
    ensure!(
        !Self::is_member(&existing_king.unwrap()),
        "current king is a member so maintains priority"
    );
    ensure!(
        Self::is_member(&new_king),
        "new king is not a member so doesn't get priority"
    );

    // <no (unnecessary) storage call here>
    // place new king
    <KingMember<T>>::put(new_king.clone());

    Self::deposit_event(Event::BetterKingSwap(old_king.unwrap(), new_king));
    Ok(().into())
}

Not all types implement Copy or Clone, so it is important to discern other patterns that minimize and alleviate the cost of calls to storage.

Quiz