はじめに
本連載では、カスタムブロックチェーンを構築するためのフレームワークであるSubstrateの概要と使い方について、全6回で解説します。
第5回では、Rustを用いて実装した独自のRuntimeをSubstrateチェーン上にデプロイし、動作を確認する手法について紹介します。
本記事は、過去にコンセンサス・ベイスが主宰していたオンラインサロンの記事です。記事は2017年~2018年にかけて執筆されたため、一部は、既に古くなっている可能性があります。あらかじめご了承ください
プロジェクトの作成
Substrateの独自Runtimeを開発するため、まずはSubstrateノードのプロジェクトを作成します。substrateコマンドから新規プロジェクトを作成することもできますが、Substrateの公式レポジトリはmasterブランチでアクティブに開発が進められており、動作が安定しません。今回は、ある程度動作が安定したSubstrateノードとUIのパッケージを提供している substrate-package レポジトリを利用します。
templateから新規ノードを作成
コマンド1を用いて、substratep-package レポジトリをダウンロードし、中のリネームスクリプトを実行します。引数には、作成したいプロジェクト名と、作者名を指定します。今回は、プロジェクト名として substrate-mytoken、作者名に myname を指定しています。
リネームが完了すると、さきほど指定したプロジェクト名のフォルダが作成されているはずなので、そのフォルダに移動し、最初のビルドを行います。この処理には、環境にもよりますが、おおよそ数十分の時間がかかります。
コマンド1. Substrate新規ノードのプロジェクト作成
$ git clone https://github.com/shawntabrizi/substrate-package
$ cd substrate-package
$ ./substrate-package-rename.sh substrate-mytoken myname
$ cd substrate-mytoken
$ ./scripts/build.sh
$ cargo build --release
新規モジュールを追加
本稿では、サンプルとして最低限の機能を実装したTokenのRuntimeを実装してみます。コマンド2を用いて、新たに「token」という名前のモジュールをプロジェクトに追加します。
処理が成功すると、runtime/src以下のフォルダに、新たに token.rs というファイルが追加されているはずです。
コマンド2. tokenモジュールの新規作成
$ substrate-module-new token
Substrate Module Setup
Creating module in runtime/src...
Customising module...
SRML module created as runtime/src/token.rs and added to git.
Ensure that you include in your runtime/src/lib.rs the line:
mod token;
$ ls runtime/src/
lib.rs template.rs token.rs
token.rsファイルを作成しただけでは、まだモジュールとして機能しません。あらかじめ存在している runtime/src/lib.rs を編集して、新しい token モジュールを読み込む記述の追加が必要です。
runtime/src/lib.rs ファイルをエディタで開き、下記コード1からコード3に示す3箇所に、tokenモジュールに関する記述を追記します。
コード1. runtime/src/lib.rs: mod tokenの追記
/// Used for the module template in `./template.rs`
mod template;
mod token;
コード2. runtime/src/lib.rs:token::Traitの追記
/// Used for the module template in `./template.rs`
impl template::Trait for Runtime {
type Event = Event;
}
impl token::Trait for Runtime {
type Event = Event;
}
コード3. runtime/src/lib.rs: construct_runtimeマクロにTokenを追記
construct_runtime!(
pub enum Runtime with Log(InternalLog: DigestItem) where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
System: system::{default, Log(ChangesTrieRoot)},
Timestamp: timestamp::{Module, Call, Storage, Config, Inherent},
Consensus: consensus::{Module, Call, Storage, Config, Log(AuthoritiesChange), Inherent},
Aura: aura::{Module},
Indices: indices,
Balances: balances,
Sudo: sudo,
// Used for the module template in `./template.rs`
TemplateModule: template::{Module, Call, Storage, Event},
Token: token::{Module, Call, Storage, Event},
ExampleModule: substrate_module_template::{Module, Call, Storage, Event},
}
);
ビルド
lib.rsの編集が完了したら、コマンド3を用いてプロジェクトを再度ビルドし、Tokenモジュールが読み込まれているかを確認してみましょう。
substrateのpurge-chainサブコマンドは、チェーンの状態を初期化するコマンドです。チェーンのRuntimeに修正をおこなっても、すでにブロックが存在していると変更が反映されない場合があるので、その場合は purge-chain を実行しましょう。
コマンド3. プロジェクトのビルドとノードの起動
$ ./scripts/build.sh
$ cargo build --release
$ ./target/release/substrate-mytoken purge-chain --dev -y
$ ./target/release/substrate-mytoken --dev
コンパイルと起動がうまく成功したら、Polkadot/Substrate Portal (https://polkadot.js.org/apps/#/explorer)にブラウザでアクセスし、ノードの状態を確認します。
図1に示すとおり、Chain stateのプルダウンにtokenモジュールが読み込まれていれば問題ありません。
図1. Chain stateの確認
シンプルなトークンRuntimeの実装
それでは、最低限の機能をもった独自のトークンRuntimeを実装していきます。今回は、トークンの初期化の際にトークンの流通量を定義し、トークンを初期化したオーナーアカウントが発行されたすべてのトークンを保持し、他のアカウントにトークンを送金することができる、というシンプルなトークンを実装します。
まず、token.rsをエディタで開き、上部のuse文で必要なモジュールを追記します。今回は、データのバリデーションをおこなうためのensureやensure_signed, アカウントとトークン残高のマッピングを表現するためのStorageMapを追記します。
コード4. token.rs: use文の修正
use support::{
decl_module, decl_storage, decl_event, StorageValue,
dispatch::Result, ensure, StorageMap
};
use system::ensure_signed;
続いて、token.rs上でdecl_storageマクロの箇所を探し、必要なデータ構造を定義します。今回は、トークンが初期化されたかどうかを表す Init、トークンの所有者である Owner、トークンの流通量である Supply、各アカウントのトークン残高を表すBalanceOfを定義します。
ここで、boolは真偽値、u128は128ビットのunsinged intを表します。T::AccountIdは、アカウントを表現するためにSubstrateのパッケージであらかじめ定義されているデータ型です。
なお、トークンの名前やティッカーシンボルなど、文字列を扱いたいこともあると思いますが、Substrateのストレージで文字列を扱うには、Vec
コード5. token.rs: decl_storageマクロの修正
decl_storage! {
trait Store for Module as token {
Init get(is_init): bool;
Owner get(owner): T::AccountId;
Supply get(supply): u128;
BalanceOf get(balance_of): map T::AccountId => u128;
}
}
続いて、token.rsのdecl_moduleマクロを探し、initとtransfer関数を定義します。第1引数には、関数を実行したアカウント(origin)が入ります。init関数は、その他にトークン供給量(supply: u128)を引数に取り、transfer関数は送信先アドレス(receiver: T::AccountId)と送金額(value: u128)を取ります。
コード6. token.rs: decl_moduleマクロの修正
decl_module! {
pub struct Module for enum Call where origin: T::Origin {
fn deposit_event() = default;
fn init(origin, supply: u128) -> Result {
let sender = ensure_signed(origin)?;
ensure!(Self::is_init() == false, "Already initialized.");
>::put(supply);
>::insert(sender.clone(), supply);
>::put(sender.clone());
>::put(true);
Ok(())
}
fn transfer(_origin, receiver: T::AccountId, value: u128) -> Result {
let sender = ensure_signed(_origin)?;
ensure!(>::exists(sender.clone()),
"Account does not own this token");
ensure!(sender != receiver,
"Account can not send token to yourself");
let sender_balance = Self::balance_of(sender.clone());
ensure!(sender_balance >= value, "Not enough balance.");
let updated_sender_balance = sender_balance - value;
let receiver_balance = Self::balance_of(receiver.clone());
let updated_receiver_balance = receiver_balance + value;
>::insert(sender.clone(), updated_sender_balance);
>::insert(receiver.clone(), updated_receiver_balance);
Ok(())
}
}
}
ここまでの修正が完了したら、コマンド4を用いて、再度ビルドとノードの実行をおこないます。
コマンド4. プロジェクトの再ビルドとノード再実行
$ ./scripts/build.sh
$ cargo build --release
$ ./target/release/substrate-mytoken purge-chain --dev -y
$ ./target/release/substrate-mytoken --dev
ブラウザでPolkadot/Substrate Portalを開き、再度Chain stateのtokenを確認すると、さきほどストレージの箇所で記述したbalanceOfやinit, owner, supplyなどがプルダウンに表示されているはずです。
図2. tokenのストレージ一覧を確認
それでは、実装したinitとtransfer関数の動作を確認してみましょう。
サイドバーからExtrinsicsのページに移動し、アカウントをAlice、extrinsicをtokenに指定し、init(supply)関数を実行します。supplyとして、適当な数字を指定します。
図3. token.init関数の実行
問題なくinit関数が実行されれば、AliceがOwnerとして登録され、InitやSupplyの値が更新されているはずです。結果を確認したい方は、サイドバーからもう一度Chain stateを選択し、ストレージのデータを確認してみてください。
続いて、transfer確認の動作を確認してみましょう。ExtrinsicsからアカウントをAlice、extrinsicをtokenに指定し、transfer(receiver, value)を実行します。引数として、receiverにはBobのアカウント、valueにはsupply以下の適当な値を指定します。
無事にtransfer関数が実行されれば、AliceやBobのトークン残高(BalanceOf)が更新されているはずです。
図4. token.transfer関数の実行
独自データ構造の利用
ここまで見てきたサンプルコードでは、一つ重大な欠陥があります。トークンの残高を表すために単純なu128型を指定しているため、大きな数を扱った場合にオーバフローを起こしたり、0以下になる計算をしたときにアンダーフローを起こす可能性があります。
そのような危険性を回避するために、substrateのruntimeでは、安全な加算や減算をおこなうCheckedAddやCheckedSubなどの関数が用意されています。今回は、トークンの残高としてCheckedAddやCheckedSubを継承した独自のTokenBalance型を定義する改修をおこなってみましょう。
まず、コード7に示すとおり、use文で必要なモジュールのインポートを追加します。
コード7. token.rs: use文の修正
use support::{
decl_module, decl_storage, decl_event, StorageValue,
dispatch::Result, ensure, StorageMap, Parameter
};
use system::ensure_signed;
use parity_codec::Codec;
use runtime_primitives::traits::{SimpleArithmetic, CheckedSub, CheckedAdd, As};
続いて、system::Traitの記述を探し、新たにTokenBalanceというtypeを追加します。TokenBalanceには、CheckedAddやCheckedSubを実装したSimpleArithmeticというTraitの他、ストレージや関数のパラメタとして使うためのTraitを組み合わせます。
コード8. token.rs: TokenBalance型の追加
pub trait Trait: system::Trait {
type Event: From> + Into<::Event>;
type TokenBalance:
SimpleArithmetic + Codec + Default + Copy + Parameter + As;
}
さきほど定義したTokenBalance型を用いて、decl_storage中のSupplyとBalanceOfの型を修正します。
コード9. token.rs: decl_storageマクロの修正
decl_storage! {
trait Store for Module as token {
Init get(is_init): bool;
Owner get(owner): T::AccountId;
Supply get(supply): T::TokenBalance;
BalanceOf get(balance_of): map T::AccountId => T::TokenBalance;
}
}
Storageと同様に、initとtransferの関数も修正します。今回は、UIとのつなぎ込みを簡潔にするため、引数の型はu128のままとし、内部でT::TokenBalance型に変換する方法をとっています。
transfer関数において、送信元や送信先のトークン残高を更新する箇所で、+や-などの演算の代わりに、checked_addやchecked_subのメソッドを用います。もし、オーバフローのエラーが発生した場合、ok_orで指定したエラーメッセージが出力されます。
コード10. token.rs: decl_moduleマクロの更新
decl_module! {
pub struct Module for enum Call where origin: T::Origin {
fn deposit_event() = default;
fn init(origin, supply: u128) -> Result {
let sender = ensure_signed(origin)?;
let supply = >::sa(supply);
ensure!(Self::is_init() == false, "Already initialized.");
>::put(supply);
>::insert(sender.clone(), supply);
>::put(sender.clone());
>::put(true);
Ok(())
}
fn transfer(_origin, receiver: T::AccountId, value: u128) -> Result {
let sender = ensure_signed(_origin)?;
let value = >::sa(value);
ensure!(>::exists(sender.clone()),
"Account does not own this token");
ensure!(sender != receiver,
"Account can not send token to yourself");
let sender_balance = Self::balance_of(sender.clone());
ensure!(sender_balance >= value, "Not enough balance.");
let updated_sender_balance =
sender_balance.checked_sub(&value).ok_or("overflow")?;
let receiver_balance = Self::balance_of(receiver.clone());
let updated_receiver_balance =
receiver_balance.checked_add(&value).ok_or("overflow")?;
>::insert(sender.clone(), updated_sender_balance);
>::insert(receiver.clone(), updated_receiver_balance);
Ok(())
}
}
}
また、lib.rsのtoken::Trait for Runtimeの箇所を再度開き、TokenBalanceの記述を追記します。
コード11. lib.rs: TokenBalanceの追記
impl token::Trait for Runtime {
type Event = Event;
type TokenBalance = u128;
}
上記の修正が完了したら、再度プロジェクトのビルドとノード再起動をおこない、動作を確認します。
独自のデータ型を使う際の注意点として、UI側は独自に定義した型の情報を知らないため、そのままでは正しくデータをデコードすることができません。
Polkadot/Substrate Portalの場合、図5のとおり、SettingsのDeveloperタブから、独自の型定義をJSON形式で指定することができます。
ここで指定するJSONによるTokenBalanceの型定義をコード12に示します。
図5. TokenBalance型定義の設定
コード12. JSON形式によるTokenBalanceの型定義
{
"TokenBalance": "u128"
}
型定義の設定が完了したら、さきほどと同様にUI上からinitやtransfer関数の動作を確認してみてください。
第6回では、今回作成したトークンRuntimeをブラウザから操作するための独自のWebアプリケーションを実装する手法について紹介します。
以上
参考文献:
- What is Substrate? – Parity
https://www.parity.io/what-is-substrate/ - Substrate Developer Hub
https://docs.substrate.dev/ - Substrate: The platform for blockchain innovators – Github
https://github.com/paritytech/substrate - polkadot-js/app – Github
https://github.com/polkadot-js/apps
ブロックチェーン学習に最適の書籍の紹介
図解即戦力 ブロックチェーンのしくみと開発がこれ1冊でしっかりわかる教科書
本書は、ブロックチェーン技術に興味を持ったエンジニアや、その仕組みを学び、自分の仕事に活かしたいビジネスパーソンを対象にして、ブロックチェーンのコア技術とネットワーク維持の仕組みを平易な言葉で解説しています。この本を読んだうえで、実際にコードを書くような専門書、ブロックチェーンビジネスの解説書を読むことで、理解度が飛躍的に高まるでしょう。(はじめにより)
会社紹介
弊社(コンセンサス・ベイス株式会社)は、2015年設立の国内で最も古いブロックチェーン専門企業です。これまでに、大手企業の顧客を中心に、日本トップクラスのブロックチェーンの開発・コンサルティング実績があります。ブロックチェーンに関わるビジネスコンサル・システム開発・教育・講演などご希望でしたら、お気軽にお問い合わせください。
会社ホームページ
https://www.consensus-base.com/
免責事項
本記事に掲載されている記事の内容につきましては、正しい情報を提供することに務めてはおりますが、提供している記事の内容及び参考資料からいかなる損失や損害などの被害が発生したとしても、弊社では責任を負いかねます。実施される際には、法律事務所にご相談ください。
技術・サービス・実装方法等のレビュー、その他解説・分析・意見につきましてはblock-chani.jp運営者の個人的見解です。正確性・正当性を保証するものではありません。本記事掲載の記事内容のご利用は読者様個人の判断により自己責任でお願いいたします。