-
Notifications
You must be signed in to change notification settings - Fork 103
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4a8eb8d
commit 7ef2ed5
Showing
164 changed files
with
5,770 additions
and
1,973 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.