1. 程式人生 > 其它 >【Substrate Collectables教程】【第2章Kitties】3 追蹤所有 Kitties

【Substrate Collectables教程】【第2章Kitties】3 追蹤所有 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 更清晰可讀; 你只需要確保記住在末尾加 ?

如果我們成功地能夠在沒有上溢的情況下遞增 AllPeopleCount,那麼它就會將新值分配給 new_all_people_count。如果失敗,我們的 module 將返回一個 Err(),它可以由我們的 runtime 優雅地處理。錯誤訊息也將直接顯示在節點的控制檯輸出中。

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;

這實際上只是 AllPeopleArray 中內容的反轉。請注意,我們這裡不需要 getter 函式,因為此儲存項只在內部使用,並且不需要作為模組 API 的一部分公開。

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(())
        }
    }
}