Skip to content

Commit

Permalink
minimize sections
Browse files Browse the repository at this point in the history
  • Loading branch information
shawntabrizi committed Aug 7, 2024
1 parent 4a8eb8d commit 7ef2ed5
Show file tree
Hide file tree
Showing 164 changed files with 5,770 additions and 1,973 deletions.
60 changes: 2 additions & 58 deletions steps/10/README.md
Original file line number Diff line number Diff line change
@@ -1,61 +1,5 @@
# Storage Basics

Now that we have covered the basics of Pallets, and gone through all of the template code, we can start writing some code ourselves.
Now that we have covered the basics of Pallets and gone through all of the template code, we can start writing some code ourselves.

In this section you will learn the basics of creating and using storage in your Pallet.

But before we can start coding, we need to learn some basics about blockchains.

## Hash Functions

Hash functions are an important tool throughout blockchain development.

A hash function takes an arbitrary sized input and returns a fixed-size string of bytes.

This output, usually called a hash, is unique to each unique input. Even a small change to the input creates a dramatic change to the output.

Hash functions have several key properties:

- Deterministic: The same input always produces the same output.
- Pre-image Resistant: It is difficult to derive the original input from its hash value.
- Collision Resistant: It’s hard to find two different inputs that produce the same hash output.

These properties make hash functions key for ensuring data integrity and uniqueness in blockchain technology.

## Hash Fingerprint

Due to the properties of a Hash, it is often referred to as a fingerprint.

For context, a 32-byte hash has 2^32 different possible outputs. This nearly as many atoms as there are in the whole universe!

This uniqueness property helps blockchain nodes come to consensus with one another.

Rather than needing to compare all the data in their blockchain database with one another, they can simply share the hash of that database, and know in a single small comparison if all data in that database is the same.

Remember, if there were any small differences between their databases, even just one bit in a multi-terabyte database being different, the resulting hash would dramatically change, and they would know their databases are not the same.

## Merkle Trie

A merkle trie is a data structure which is constructed using a hash function.

Rather than hashing the whole database into a single hash, we create a tree of hashes.

For example, we take pairs of data, combine them, then hash them to generate a new output. Then we take pairs of hashes, combine them, then hash them to generate another new output.

We can repeat this process until we are left with a single hash called the "root hash". This process literally creates a tree of hashes.

Just like before, we can use a single hash to represent the integrity of all data underneath it, but now we can efficiently represent specific pieces of data in the database using the path down the trie to that data.

It is called a merkle "trie" because the trie data structure is used to reduce the amount of redundant data stored in the tree.

### Complexity

The reason we go into this much detail about merkle tries is that they increase the complexity in reading and writing to the blockchain database.

Whereas reading and writing to a database could be considered `O(1)`, a merklized database has read and write complexity of `O(log N)`, where `N` is the total number of items stored in the database.

This additional complexity means that designing storage for a blockchain is an extremely important and sensitive operation.

The primary advantage of using a merkle trie is that proving specific data exists inside the database is much more efficient! Whereas you would normally need to share the whole database to prove that some data exists, with a merklized database, you only need to share `O(log N)` amount of data.

In this next section, and throughout the tutorial, we will start to explore some of those decisions.
In this section you will learn the basics of creating and using storage in your Pallet, including creating and using storage values and storage maps.
2 changes: 1 addition & 1 deletion 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": "section: storage basics"
}
}
68 changes: 52 additions & 16 deletions steps/11/README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,64 @@
# Storage Values
# Blockchain Storage

The most basic storage type for a blockchain is a single `StorageValue`.
Blockchains use a Merkle Trie structure to store data. The Merkle Trie provides two important properties for blockchains:

A `StorageValue` is used to place a single object into the blockchain storage.
1. Allows the whole database to be represented by a single fingerprint, which can easily be compared to other nodes.
2. Allows the creation of lightweight proofs, proving that specific data exists in the database.

A single object can be as simple as a single type like a `u32`, or more complex structures, or even vectors.
This comes at the cost of additional complexity reading and writing data to the blockchain.

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.
Let's learn about Merkle Tries in more detail.

## Construction
## Hash Functions

We constructed a simple `StorageValue` for you in the code, but let's break it down:
Hash functions are an important tool throughout blockchain development.

```rust
#[pallet::storage]
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32>;
```
A hash function takes an arbitrary sized input and returns a fixed-size string of bytes.

As you can see, our storage is a type alias for a new instance of `StorageValue`.
This output, usually called a hash, is unique to each unique input. Even a small change to the input creates a dramatic change to the output.

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`.
Hash functions have several key properties:

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.
- Deterministic: The same input always produces the same output.
- Pre-image Resistant: It is difficult to derive the original input from its hash value.
- Collision Resistant: It’s hard to find two different inputs that produce the same hash output.

Visibility of the type is up to you and your needs, but you need to remember that blockchains are public databases. So `pub` in this case is only about Rust, and allowing other modules to access this storage and its APIs directly.
These properties make hash functions key for ensuring data integrity and uniqueness in blockchain technology.

You cannot make storage on a blockchain "private", and even if you make this storage without `pub`, there are low level ways to manipulate the storage in the database.
## Hash Fingerprint

Due to the properties of a Hash, it is often referred to as a fingerprint.

For context, a 32-byte hash has 2^32 different possible outputs. This nearly as many atoms as there are in the whole universe!

This uniqueness property helps blockchain nodes come to consensus with one another.

Rather than needing to compare all the data in their blockchain database with one another, they can simply share the hash of that database, and know in a single small comparison if all data in that database is the same.

Remember, if there were any small differences between their databases, even just one bit in a multi-terabyte database being different, the resulting hash would dramatically change, and they would know their databases are not the same.

## Merkle Trie

A merkle trie is a data structure which is constructed using a hash function.

Rather than hashing the whole database into a single hash, we create a tree of hashes.

For example, we take pairs of data, combine them, then hash them to generate a new output. Then we take pairs of hashes, combine them, then hash them to generate another new output.

We can repeat this process until we are left with a single hash called the "root hash". This process literally creates a tree of hashes.

Just like before, we can use a single hash to represent the integrity of all data underneath it, but now we can efficiently represent specific pieces of data in the database using the path down the trie to that data.

It is called a merkle "trie" because the trie data structure is used to reduce the amount of redundant data stored in the tree.

### Complexity

The reason we go into this much detail about merkle tries is that they increase the complexity in reading and writing to the blockchain database.

Whereas reading and writing to a database could be considered `O(1)`, a merklized database has read and write complexity of `O(log N)`, where `N` is the total number of items stored in the database.

This additional complexity means that designing storage for a blockchain is an extremely important and sensitive operation.

The primary advantage of using a merkle trie is that proving specific data exists inside the database is much more efficient! Whereas you would normally need to share the whole database to prove that some data exists, with a merklized database, you only need to share `O(log N)` amount of data. This is very important to support light clients.

In this next section, and throughout the tutorial, we will start to explore some of those decisions.
2 changes: 1 addition & 1 deletion 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: learn about storage value"
"commitMessage": "action: blockchain storage"
}
6 changes: 0 additions & 6 deletions steps/11/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,6 @@ pub mod pallet {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}

/* 🚧 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)]
pub enum Event<T: Config> {
Expand Down
29 changes: 27 additions & 2 deletions steps/12/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
# Solution
# Storage Values

Here you will find the solution for the previous step.
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 `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.

## Construction

We constructed a simple `StorageValue` for you in the code, but let's break it down:

```rust
#[pallet::storage]
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 `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.

Visibility of the type is up to you and your needs, but you need to remember that blockchains are public databases. So `pub` in this case is only about Rust, and allowing other modules to access this storage and its APIs directly.

You cannot make storage on a blockchain "private", and even if you make this storage without `pub`, there are low level ways to manipulate the storage in the database.
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: learn about storage value"
"commitMessage": "template: learn about storage value"
}
7 changes: 5 additions & 2 deletions steps/12/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ pub mod pallet {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}

#[pallet::storage]
pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32>;
/* 🚧 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/13/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<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`.
Here you will find the solution for the previous step.
4 changes: 2 additions & 2 deletions 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: counter logic"
}
"commitMessage": "solution: learn about storage value"
}
6 changes: 0 additions & 6 deletions steps/13/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`.
- 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
52 changes: 50 additions & 2 deletions steps/14/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/14/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/14/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: u32 = 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
Loading

0 comments on commit 7ef2ed5

Please sign in to comment.