【Substrate Collectables教程】【第2章Kitties】3 追踪所有 Kitties([sub collectables tutorial] [Chapter 2 kitties] 3 track all kitties)

追踪所有 Kitties

现在我们已经让每个用户都可以创建自己独一无二的 kitty,我们开始追踪它们!

我们的游戏将会追踪创建的 kitty 总数,以及追踪谁拥有哪只 kitty。

作为基于 Substrate 框架的应用开发人员,很重要的一点是要区分 Substrate 上 runtime 的逻辑设计和 Ethereum 平台上的智能合约开发的不同。

在 Ethereum 中,如果你的交易在任何时候失败(错误,没有 gas 等…),你的智能合约的状态将不受影响。但是,在 Substrate 上并非如此。一旦交易开始修改区块链的存储,这些更改就是永久性的,即使交易在 runtime 执行期间失败也是如此。

这对于区块链系统是必要的,因为你可能想要追踪用户的 nonce 或者为任何发生的计算减去 gas 费用。对于失败的交易来说,这两件事实际上都发生在 Ethereum 状态转换函数中,但你作为智能合约开发人员,从来不必担心去管理这些事情。

既然现在你是 Substrate runtime 开发人员,你必须察觉到你对区块链状态所做的任何更改,并确保它遵循 “verify first, write last” 的模式。我们将在整个教程中帮助你做到这点。

3.1 创建一个 List

在 runtime 开发中,列表循环通常是坏事。如果没有明确对其防范,枚举一个列表的 runtime 函数会增加 O(N) 的复杂度,但是仅仅花费了 O(1) 的费用。结果就是你的链变得容易被攻击。并且,如果你所枚举的列表过大甚至是无限的,你的 runtime 可能需要比区块生成的间隔更多的时间。这意味着区块生产者不能正常地生产区块!

基于上述原因,本教程在 runtime 逻辑中不会使用任何列表循环。如果你选择使用,请确保已经考虑清楚。

作为替代,我们可以使用映射和计数器模拟列表,如下所示:

decl_storage! {
    trait Store for Module<T: Trait> as Example {
        AllPeopleArray get(person): map u32 => T::AccountId;
        AllPeopleCount get(num_of_people): u32;
    }
}

这里我们将在 runtime 中存储人员列表,用多个  表示。我们只需要小心谨慎地维护这些存储项目,以确保它们准确和最新。

AccountId

3.2 检查 Overflow/Underflow

如果你曾经在 Ethereum 上开发过,那么如果你不执行 “safe math”,你就会碰到你所熟悉的问题,即 Overflow/Underflow。Overflow 和 Underflow 很容易就可以使 runtime 出现 panic 或者存储混乱。

在更改存储状态之前,你必须始终主动检查可能的 runtime 错误。请记住,与 Ethereum 不同,当交易失败时,状态不会恢复到交易发生之前,因此你有责任确保在错误处理上不会产生任何副作用。

幸运的是,在 Rust 中检查这些类型的错误非常简单,其中原始数字类型具有  和  函数。

checked_add()
checked_sub()

假设我们想要向  中添加一项,我们首先要检查我们是否可以成功增加 ,如下所示:

AllPeopleArray
AllPeopleCount
let all_people_count = Self::num_of_people();

let new_all_people_count = all_people_count.checked_add(1).ok_or("Overflow adding a new person")?;

使用  与下面的代码相同:

ok_or
let new_all_people_count = match all_people_count.checked_add(1) {
    Some (c) => c,
    None => return Err("Overflow adding a new person"),
};

但是, 比  更清晰可读; 你只需要确保记住在末尾加 !

ok_or
match
?

如果我们成功地能够在没有上溢的情况下递增 ,那么它就会将新值分配给 。如果失败,我们的 module 将返回一个 ,它可以由我们的 runtime 优雅地处理。错误消息也将直接显示在节点的控制台输出中。

AllPeopleCount
new_all_people_count
Err()

3.3 更新存储中的 List

现在我们已经检查过了,我们可以安全地增加列表项,我们最终可以将更改推送到存储中。请记住,当你更新列表时,列表的 “最后一个索引” 比计数少一个。例如,在包含 2 个项的列表中,第一个项是索引 0,第二个项是索引 1。

将新的人员添加到我们的人员列表中,完整示例如下所示:

fn add_person(origin, new_person: T::AccountId) -> Result {
    let sender = ensure_signed(origin)?;

    let all_people_count = Self::num_of_friends();

    let new_all_people_count = all_people_count.checked_add(1).ok_or("Overflow adding a new person")?;

    <AllPeopleArray<T>>::insert(all_people_count, new_people);
    <AllPeopleCount<T>>::put(new_all_people_count);

    Ok(())
}

我们也应该为这个函数添加碰撞检测!你还记得怎么做吗?

3.4 删除 List 元素

当我们尝试从列表中间删除元素时,映射和计数模式引入的一个问题就是会在列表中留下空位。幸运的是,在本教程中我们管理的列表的顺序并不重要,因此我们可以使用 “swap and pop” 的方法来有效地缓解此问题。

“swap and pop” 方法交换删除项的位置以及列表中的最后一项。然后,我们可以简单地删除最后一项而不会在我们的列表中引入任何空位。

我们不会在每次删除时运行循环来查找删除项的索引,而是使用一些额外的存储来追踪列表中每个项及其所在的位置。

我们现在不会引入 “swap and pop” 的逻辑,但是我们会要求你使用一个  存储项来追踪列表中每项的索引,如下所示:

Index
AllPeopleIndex: map T::AccountId => u32;

这实际上只是  中内容的反转。请注意,我们这里不需要 getter 函数,因为此存储项只在内部使用,并且不需要作为模块 API 的一部分公开。

AllPeopleArray

3.5 示例

use support::{decl_storage, decl_module, StorageValue, StorageMap,
    dispatch::Result, ensure, decl_event};
use system::ensure_signed;
use runtime_primitives::traits::{As, Hash};
use parity_codec::{Encode, Decode};

#[derive(Encode, Decode, Default, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct Kitty<Hash, Balance> {
    id: Hash,
    dna: Hash,
    price: Balance,
    gen: u64,
}

pub trait Trait: balances::Trait {
    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
}

decl_event!(
    pub enum Event<T>
    where
        <T as system::Trait>::AccountId,
        <T as system::Trait>::Hash
    {
        Created(AccountId, Hash),
    }
);

decl_storage! {
    trait Store for Module<T: Trait> as KittyStorage {
        Kitties get(kitty): map T::Hash => Kitty<T::Hash, T::Balance>;
        KittyOwner get(owner_of): map T::Hash => Option<T::AccountId>;

        AllKittiesArray get(kitty_by_index): map u64 => T::Hash;
        AllKittiesCount get(all_kitties_count): u64;
        AllKittiesIndex: map T::Hash => u64;

        OwnedKitty get(kitty_of_owner): map T::AccountId => T::Hash;

        Nonce: u64;
    }
}

decl_module! {
    pub struct Module<T: Trait> for enum Call where origin: T::Origin {

        fn deposit_event<T>() = default;

        fn create_kitty(origin) -> Result {
            let sender = ensure_signed(origin)?;

            let all_kitties_count = Self::all_kitties_count();

            let new_all_kitties_count = all_kitties_count.checked_add(1)
            .ok_or("Overflow adding a new kitty to total supply")?;

            let nonce = <Nonce<T>>::get();
            let random_hash = (<system::Module<T>>::random_seed(), &sender, nonce)
                .using_encoded(<T as system::Trait>::Hashing::hash);

            ensure!(!<KittyOwner<T>>::exists(random_hash), "Kitty already exists");

            let new_kitty = Kitty {
                id: random_hash,
                dna: random_hash,
                price: <T::Balance as As<u64>>::sa(0),
                gen: 0,
            };

            <Kitties<T>>::insert(random_hash, new_kitty);
            <KittyOwner<T>>::insert(random_hash, &sender);

            <AllKittiesArray<T>>::insert(all_kitties_count, random_hash);
            <AllKittiesCount<T>>::put(new_all_kitties_count);
            <AllKittiesIndex<T>>::insert(random_hash, all_kitties_count);

            <OwnedKitty<T>>::insert(&sender, random_hash);

            <Nonce<T>>::mutate(|n| *n += 1);

            Self::deposit_event(RawEvent::Created(sender, random_hash));

            Ok(())
        }
    }
}
————————

追踪所有 Kitties

Now that we have enabled each user to create their own unique Kitty, we start tracking them!

Our game will track the total number of Kitty created and who owns which Kitty.

As an application developer based on the substrate framework, it is important to distinguish between the logical design of runtime on substrate and the development of smart contract on Ethereum platform.

In Ethereum, if your transaction fails at any time (error, no gas, etc.), The status of your smart contract will not be affected. However, this is not the case on substrate. Once the transaction starts to modify the storage of the blockchain, these changes are permanent, even if the transaction fails during runtime execution.

This is necessary for blockchain systems because you may want to track users’ nonces or subtract gas charges for any calculations that occur. For failed transactions, these two things actually happen in the Ethereum state transition function, but as a smart contract developer, you never have to worry about managing these things.

Now that you are a substrate runtime developer, you must be aware of any changes you make to the blockchain status and ensure that it follows the “verify first, write last” pattern. We will help you do this throughout the tutorial.

3.1 # create a list

In runtime development, list loops are often a bad thing. If there is no explicit prevention, enumerating the runtime function of a list will increase the complexity of O (n), but it only costs o (1). The result is that your chain becomes vulnerable. Also, if the list you enumerate is too large or even infinite, your runtime may need more time than the interval between block generation. This means that block producers cannot normally produce blocks!

For these reasons, this tutorial does not use any list loops in runtime logic. If you choose to use it, please make sure you have considered it clearly.

Instead, we can use mapping and counter emulation lists as follows:

decl_storage! {
    trait Store for Module<T: Trait> as Example {
        AllPeopleArray get(person): map u32 => T::AccountId;
        AllPeopleCount get(num_of_people): u32;
    }
}

Here, we will store the personnel list in the runtime, which is represented by multiple}. We just need to maintain these storage items carefully to ensure that they are accurate and up-to-date.

AccountId

3.2 检查 Overflow/Underflow

If you have ever developed on Ethereum, if you do not execute “safe math”, you will encounter a problem you are familiar with, namely overflow / underflow. Overflow and underflow can easily cause panic or storage confusion in the runtime.

You must always proactively check for possible runtime errors before changing the storage state. Remember, unlike Ethereum, when a transaction fails, the state will not return to before the transaction occurs, so it is your responsibility to ensure that there are no side effects on error handling.

Fortunately, it’s easy to check for these types of errors in rust, where the original number type has the , and , functions.

checked_add()
checked_sub()

Suppose we want to add an item to , we first need to check whether we can successfully add it, as shown below:

AllPeopleArray
AllPeopleCount
let all_people_count = Self::num_of_people();

let new_all_people_count = all_people_count.checked_add(1).ok_or("Overflow adding a new person")?;

Use the same code as the following:

ok_or
let new_all_people_count = match all_people_count.checked_add(1) {
    Some (c) => c,
    None => return Err("Overflow adding a new person"),
};

However, it is clearer and more readable than; You just need to make sure you remember to add at the end!

ok_or
match
?

If we succeed in incrementing without overflow, it will assign the new value to. If it fails, our module will return one, which can be handled gracefully by our runtime. Error messages are also displayed directly in the node’s console output.

AllPeopleCount
new_all_people_count
Err()

3.3 update the list in the storage

Now that we have checked, we can safely add list items, and we can finally push the changes to storage. Remember that when you update the list, the “last index” of the list is one less than the count. For example, in a list of 2 items, the first item is index 0 and the second item is index 1.

Add a new person to our people list. The complete example is as follows:

fn add_person(origin, new_person: T::AccountId) -> Result {
    let sender = ensure_signed(origin)?;

    let all_people_count = Self::num_of_friends();

    let new_all_people_count = all_people_count.checked_add(1).ok_or("Overflow adding a new person")?;

    <AllPeopleArray<T>>::insert(all_people_count, new_people);
    <AllPeopleCount<T>>::put(new_all_people_count);

    Ok(())
}

We should also add collision detection to this function! Do you remember how to do it?

3.4} delete list element

When we try to remove elements from the middle of the list, one of the problems introduced by the mapping and counting mode is to leave empty spaces in the list. Fortunately, the order of the lists we manage in this tutorial is not important, so we can use the “swap and pop” method to effectively alleviate this problem.

The “swap and pop” method swaps the location of the deleted item and the last item in the list. Then, we can simply delete the last item without introducing any vacancies in our list.

Instead of running a loop to find the index of deleted items every time we delete, we use some additional storage to track each item in the list and its location.

We will not introduce the logic of “swap and pop” now, but we will ask you to use a storage item to track the index of each item in the list, as shown below:

Index
AllPeopleIndex: map T::AccountId => u32;

This is actually just a reversal of the content in. Please note that we do not need the getter function here, because this storage item is only used internally and does not need to be exposed as part of the module API.

AllPeopleArray

3.5 examples

use support::{decl_storage, decl_module, StorageValue, StorageMap,
    dispatch::Result, ensure, decl_event};
use system::ensure_signed;
use runtime_primitives::traits::{As, Hash};
use parity_codec::{Encode, Decode};

#[derive(Encode, Decode, Default, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct Kitty<Hash, Balance> {
    id: Hash,
    dna: Hash,
    price: Balance,
    gen: u64,
}

pub trait Trait: balances::Trait {
    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
}

decl_event!(
    pub enum Event<T>
    where
        <T as system::Trait>::AccountId,
        <T as system::Trait>::Hash
    {
        Created(AccountId, Hash),
    }
);

decl_storage! {
    trait Store for Module<T: Trait> as KittyStorage {
        Kitties get(kitty): map T::Hash => Kitty<T::Hash, T::Balance>;
        KittyOwner get(owner_of): map T::Hash => Option<T::AccountId>;

        AllKittiesArray get(kitty_by_index): map u64 => T::Hash;
        AllKittiesCount get(all_kitties_count): u64;
        AllKittiesIndex: map T::Hash => u64;

        OwnedKitty get(kitty_of_owner): map T::AccountId => T::Hash;

        Nonce: u64;
    }
}

decl_module! {
    pub struct Module<T: Trait> for enum Call where origin: T::Origin {

        fn deposit_event<T>() = default;

        fn create_kitty(origin) -> Result {
            let sender = ensure_signed(origin)?;

            let all_kitties_count = Self::all_kitties_count();

            let new_all_kitties_count = all_kitties_count.checked_add(1)
            .ok_or("Overflow adding a new kitty to total supply")?;

            let nonce = <Nonce<T>>::get();
            let random_hash = (<system::Module<T>>::random_seed(), &sender, nonce)
                .using_encoded(<T as system::Trait>::Hashing::hash);

            ensure!(!<KittyOwner<T>>::exists(random_hash), "Kitty already exists");

            let new_kitty = Kitty {
                id: random_hash,
                dna: random_hash,
                price: <T::Balance as As<u64>>::sa(0),
                gen: 0,
            };

            <Kitties<T>>::insert(random_hash, new_kitty);
            <KittyOwner<T>>::insert(random_hash, &sender);

            <AllKittiesArray<T>>::insert(all_kitties_count, random_hash);
            <AllKittiesCount<T>>::put(new_all_kitties_count);
            <AllKittiesIndex<T>>::insert(random_hash, all_kitties_count);

            <OwnedKitty<T>>::insert(&sender, random_hash);

            <Nonce<T>>::mutate(|n| *n += 1);

            Self::deposit_event(RawEvent::Created(sender, random_hash));

            Ok(())
        }
    }
}