Skip to content

Commit

Permalink
big change around storage value
Browse files Browse the repository at this point in the history
  • Loading branch information
shawntabrizi committed Aug 4, 2024
1 parent b9cf00c commit 4a972af
Show file tree
Hide file tree
Showing 159 changed files with 5,647 additions and 1,845 deletions.
6 changes: 3 additions & 3 deletions steps/10/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The most basic storage type for a blockchain is a single `StorageValue`.

A `StorageValue` is used to place a single object into the blockchain storage.

A single object can be as simple as a single type like a `u64`, or more complex structures, or even vectors.
A single object can be as simple as a single type like a `u32`, or more complex structures, or even vectors.

What is most important to understand is that a `StorageValue` places a single entry into the merkle trie. So when you read data, you read all of it. When you write data, you write all of it. This is in contrast to a `StorageMap`, which you will learn about next.

Expand All @@ -14,12 +14,12 @@ We constructed a simple `StorageValue` for you in the code, but let's break it d

```rust
#[pallet::storage]
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u64>;
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32>;
```

As you can see, our storage is a type alias for a new instance of `StorageValue`.

Our storage value has a parameter `Value` where we can define the type we want to place in storage. In this case, it is a simple `u64`.
Our storage value has a parameter `Value` where we can define the type we want to place in storage. In this case, it is a simple `u32`.

You will also notice `CountForKitties` is generic over `<T: Config>`. All of our storage must be generic over `<T: Config>` even if we are not using it directly. Macros use this generic parameter to fill in behind the scene details to make the `StorageValue` work. Think about all the code behind the scenes which actually sticks this storage into a merkle trie in the database of our blockchain.

Expand Down
4 changes: 2 additions & 2 deletions steps/10/gitorial_metadata.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"_Note": "This file will not be included in your final gitorial.",
"commitMessage": "action: learn about storage value"
}
"commitMessage": "template: learn about storage value"
}
8 changes: 5 additions & 3 deletions steps/10/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ pub mod pallet {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}

/* 🚧 TODO 🚧: Learn about storage value. */
#[pallet::storage]
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u64>;
/* 🚧 TODO 🚧:
- Create a new `StorageValue` named `CountForKitties`.
- `CountForKitties` should be generic over `<T: Config>`.
- Set `Value` to `u32` to store that type.
*/

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
Expand Down
52 changes: 2 additions & 50 deletions steps/11/README.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,3 @@
# Kitty Counter
# Solution

Let's now learn how to use our new `StorageValue`.

## Basic APIs

This tutorial will only go over just the basic APIs needed to build our Pallet.

Check out the [`StorageValue` documentation](https://docs.rs/frame-support/37.0.0/frame_support/storage/types/struct.StorageValue.html) if you want to see the full APIs.

### Reading Storage

To read the current value of a `StorageValue`, you can simply call the `get` API:

```rust
let maybe_count: Option<u64> = CountForKitties::<T>::get();
```

A few things to note here.

The most obvious one is that `get` returns an `Option`, rather than the type itself.

In fact, all storage in a blockchain is an `Option`: either there is some data in the database or there isn't.

In this context, when there is no value in storage for the `CountForKitties`, we probably mean that the `CountForKitties` is zero.

So we can write the following to handle this ergonomically:

```rust
let count: u64 = CountForKitties::<T>::get().unwrap_or(0);
```

Now, whenever `CountForKitties` returns `Some(count)`, we will simply unwrap that count and directly access the `u64`. If it returns `None`, we will simply return `0u64` instead.

The other thing to note is the generic `<T>` that we need to include. You better get used to this, we will be using `<T>` everywhere! But remember, in our definition of `CountForKitties`, it was a type generic over `<T: Config>`, and thus we need to include `<T>` to access any of the APIs.

### Writing Storage

To set the current value of a `StorageValue`, you can simply call the `set` API:

```rust
CountForKitties::<T>::set(Some(1u64));
```

This storage API cannot fail, so there is no error handling needed. You just set the value directly in storage. Note that `set` will also happily replace any existing value there, so you will need to use other APIs like `exists` or `get` to check if a value is already in storage.

If you `set` the storage to `None`, it is the same as deleting the storage item.

## Your Turn

Now that you know the basics of reading and writing to storage, add the logic needed to increment the `CountForKitties` storage whenever we call `mint`.
Here you will find the solution for the previous step.
4 changes: 2 additions & 2 deletions steps/11/gitorial_metadata.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"_Note": "This file will not be included in your final gitorial.",
"commitMessage": "template: counter logic"
}
"commitMessage": "solution: learn about storage value"
}
6 changes: 0 additions & 6 deletions steps/11/src/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@ use frame::prelude::*;

impl<T: Config> Pallet<T> {
pub fn mint(owner: T::AccountId) -> DispatchResult {
/* 🚧 TODO 🚧:
- `get` the current count of kitties.
- `unwrap_or` set the count to `0`.
- increment the count by one.
- `set` the new count of kitties.
*/
Self::deposit_event(Event::<T>::Created { owner });
Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion steps/11/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub mod pallet {
}

#[pallet::storage]
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u64>;
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32>;

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
Expand Down
52 changes: 50 additions & 2 deletions steps/12/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,51 @@
# Solution
# Kitty Counter

Here you will find the solution for the previous step.
Let's now learn how to use our new `StorageValue`.

## Basic APIs

This tutorial will only go over just the basic APIs needed to build our Pallet.

Check out the [`StorageValue` documentation](https://docs.rs/frame-support/37.0.0/frame_support/storage/types/struct.StorageValue.html) if you want to see the full APIs.

### Reading Storage

To read the current value of a `StorageValue`, you can simply call the `get` API:

```rust
let maybe_count: Option<u32> = CountForKitties::<T>::get();
```

A few things to note here.

The most obvious one is that `get` returns an `Option`, rather than the type itself.

In fact, all storage in a blockchain is an `Option`: either there is some data in the database or there isn't.

In this context, when there is no value in storage for the `CountForKitties`, we probably mean that the `CountForKitties` is zero.

So we can write the following to handle this ergonomically:

```rust
let current_count: u32 = CountForKitties::<T>::get().unwrap_or(0);
```

Now, whenever `CountForKitties` returns `Some(count)`, we will simply unwrap that count and directly access the `u32`. If it returns `None`, we will simply return `0u32` instead.

The other thing to note is the generic `<T>` that we need to include. You better get used to this, we will be using `<T>` everywhere! But remember, in our definition of `CountForKitties`, it was a type generic over `<T: Config>`, and thus we need to include `<T>` to access any of the APIs.

### Writing Storage

To set the current value of a `StorageValue`, you can simply call the `set` API:

```rust
CountForKitties::<T>::set(Some(1u32));
```

This storage API cannot fail, so there is no error handling needed. You just set the value directly in storage. Note that `set` will also happily replace any existing value there, so you will need to use other APIs like `exists` or `get` to check if a value is already in storage.

If you `set` the storage to `None`, it is the same as deleting the storage item.

## Your Turn

Now that you know the basics of reading and writing to storage, add the logic needed to increment the `CountForKitties` storage whenever we call `mint`.
2 changes: 1 addition & 1 deletion steps/12/gitorial_metadata.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"_Note": "This file will not be included in your final gitorial.",
"commitMessage": "solution: counter logic"
"commitMessage": "template: counter logic"
}
9 changes: 6 additions & 3 deletions steps/12/src/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ use frame::prelude::*;

impl<T: Config> Pallet<T> {
pub fn mint(owner: T::AccountId) -> DispatchResult {
let current_count: u64 = CountForKitties::<T>::get().unwrap_or(0);
let new_count = current_count + 1;
CountForKitties::<T>::set(Some(new_count));
/* 🚧 TODO 🚧:
- `get` the `current_count` of kitties.
- `unwrap_or` set the count to `0`.
- Create `new_count` by adding one to the `current_count`.
- `set` the `new_count` of kitties.
*/
Self::deposit_event(Event::<T>::Created { owner });
Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion steps/12/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub mod pallet {
}

#[pallet::storage]
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u64>;
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32>;

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
Expand Down
142 changes: 2 additions & 140 deletions steps/13/README.md
Original file line number Diff line number Diff line change
@@ -1,141 +1,3 @@
# Safety First
# Solution

If you look into the history of "hacks" and "bugs" that happen in the blockchain world, a lot of it is associated with some kind of "unsafe" code.

Keeping our blockchain logic safe, and Rust is designed to handle it well.

## Errors

When talking about handling safe math, we will start to introduce and use errors.

### Do Not Panic!

If there is only one thing you remember after this whole tutorial, it should be this fact:

**You cannot panic inside the runtime.**

As a runtime developer, you are building logic in the low level parts of your blockchain.

A smart contract system must be able to handle malicious developers, but this comes at a performance cost.

When you program directly in the runtime, you get the highest performance possible, but you are also expected to be a competent developer and a good actor.

In short, if you introduce a panic in your code, you make your blockchain vulnerable to DDoS attacks.

But there is no reason you would ever need to panic because Rust has a great error handling system that we take advantage of in FRAME.

### Pallet Errors

All of our callable functions use the `DispatchResult` type. This means that we can always propagate up any errors that our Pallet runs into, and handle them properly, versus needing to panic.

The [`DispatchResult`](https://docs.rs/frame-support/37.0.0/frame_support/dispatch/type.DispatchResult.html) type expects either `Ok(())` or `Err(DispatchError)`.

The [`DispatchError`](https://docs.rs/frame-support/37.0.0/frame_support/pallet_prelude/enum.DispatchError.html) type has a few variants that you can easily construct / use.

For example, if you want to be a little lazy, you can simply return a `&'static str`:

```rust
fn always_error() -> DispatchResult {
return Err("this function always errors".into())
}
```

But the better option is to return a custom Pallet Error:

```rust
fn custom_error() -> DispatchResult {
return Err(Error::<T>::CustomPalletError.into())
}
```

Notice in both of these cases we had to call `into()` to convert our input type into the `DispatchError` type.

To create `CustomPalletError` or whatever error you want, you simply add a new variants to the `enum Error<T>` type.

```rust
#[pallet::error]
pub enum Error<T> {
/// This is a description for the error.
///
/// This description can be shown to the user in UIs, so make it descriptive.
CustomPalletError,
}
```

We will show you the common ergonomic ways to use Pallet Errors going forward.

## Math

### Unsafe Math

The basic math operators in Rust are **unsafe**.

Imagine our `CountForKitties` was already at the limit of `u64::MAX`. What would happen if we tried to call `mint` one more time?

We would get an overflow!

In tests `u64::MAX + 1` will actually trigger a panic, but in a `release` build, this overflow will happen silently...

And this would be really bad. Now our count would be back to 0, and if we had any logic which depended on this count being accurate, that logic would be broken.

In blockchain systems, these can literally be billion dollar bugs, so let's look at how we can do math safely.

### Checked Math

The first choice for doing safe math is to use `checked_*` APIs, for example [`checked_add`](https://docs.rs/num/latest/num/trait.CheckedAdd.html).

The checked math APIs will check if there are any underflows or overflows, and return `None` in those cases. Otherwise, if the math operation is calculated without error, it returns `Some(result)`.

Here is a verbose way you could handle checked math in a Pallet:

```rust
let final_result: u64 = match value_a.checked_add(value_b) {
Some(result) => result,
None => return Err(Error::<T>::CustomPalletError.into()),
};
```

You can see how we can directly assign the `u64` value to `final_result`, otherwise it will return an error.

We can also do this as a one-liner, which is more ergonomic and preferred:

```rust
let final_result: u64 = value_a.checked_add(value_b).ok_or(Error::<T>::CustomPalletError)?;
```

This is exactly how you should be writing all the safe math inside your Pallet.

Note that we didn't need to call `.into()` in this case, because `?` already does this!

### Saturating Math

The other option for safe math is to use `saturating_*` APIs, for example [`saturating_add`](https://docs.rs/num/latest/num/traits/trait.SaturatingAdd.html).

This option is useful because it is safe and does NOT return an `Option`.

Instead, it performs the math operations and keeps the value at the numerical limits, rather than overflowing. For example:

```rust
let value_a: u64 = 1;
let value_b: u64 = u64::MAX;
let result: u64 = value_a.saturating_add(value_b);
assert!(result == u64::MAX);
```

This generally is NOT the preferred API to use because usually you want to handle situations where an overflow would occur. Overflows and underflows usually indicate something "bad" is happening.

However, there are times where you need to do math inside of functions where you cannot return a Result, and for that, saturating math might make sense.

There are also times where you might want to perform the operation no matter that an underflow / overflow would occur. For example, imagine you made a function `slash` which slashes the balance of a malicious user. Your slash function may have some input parameter `amount` which says how much we should slash from the user.

In a situation like this, it would make sense to use `saturating_sub` because we definitely want to slash as much as we can, even if we intended to slash more. The alternative would be returning an error, and not slashing anything!

Anyway, every bone in your body should generally prefer to use the `checked_*` APIs, and handle all errors explicitly, but this is yet another tool in your pocket when it makes sense to use it.

## Your Turn

We covered a lot in this section, but the concepts here are super important.

Feel fre to reach this section again right now, and again at the end of the tutorial.

Now that you know how to ergonomically do safe math, update your Pallet to handle the `mint` logic safely and return a custom Pallet Error if an overflow would occur.
Here you will find the solution for the previous step.
2 changes: 1 addition & 1 deletion steps/13/gitorial_metadata.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"_Note": "This file will not be included in your final gitorial.",
"commitMessage": "template: safe math and errors"
"commitMessage": "solution: counter logic"
}
3 changes: 1 addition & 2 deletions steps/13/src/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ use frame::prelude::*;

impl<T: Config> Pallet<T> {
pub fn mint(owner: T::AccountId) -> DispatchResult {
let current_count: u64 = CountForKitties::<T>::get().unwrap_or(0);
/* 🚧 TODO 🚧: Update this logic to use safe math. */
let current_count: u32 = CountForKitties::<T>::get().unwrap_or(0);
let new_count = current_count + 1;
CountForKitties::<T>::set(Some(new_count));
Self::deposit_event(Event::<T>::Created { owner });
Expand Down
8 changes: 2 additions & 6 deletions steps/13/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub mod pallet {
}

#[pallet::storage]
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u64>;
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32>;

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
Expand All @@ -27,11 +27,7 @@ pub mod pallet {
}

#[pallet::error]
pub enum Error<T> {
/* 🚧 TODO 🚧:
- Introduce a new error `TooManyKitties`.
*/
}
pub enum Error<T> {}

#[pallet::call]
impl<T: Config> Pallet<T> {
Expand Down
Loading

0 comments on commit 4a972af

Please sign in to comment.