Substrate & Ink Part 1 ~スマートコントラクト再入門~
ブロックチェーンの開発フレームワークであるSubstrateによってスクラッチから各コンポーネントを作成することなく複数チェーンを接続するブロックチェーンが作成できるようになりました(Polkadotネットワークのパラチェーンとして)。Ink(ink!)はSubstrate上でスマートコントラクトを書くための『eDSL』です。この記事ではInkを使用してNFT(Non-Fungible Token)を実装してみたいと思います。
Ink(ink!)はParityが提供するSubstrateベースのブロックチェーンでスマートコントラクトを記述する『eDSL』です。[1]
Ink (or ink! as the name is commonly termed in documentation) is Parity’s solution to writing smart contracts for a Substrate based blockchain.
または
Ink is an eDSL to write WebAssembly based smart contracts using the Rust programming language.
この記事ではInkの導入とInkを使用したNFT(Non-Fungible Token)の実装をSubstrate上で実装します。参考とした元ネタ記事はこちらです。[2] [3]
やりたいこと。Part 1ではInkの概要とNFTスマートコントラクトのコードまで確認し、以降でデプロイやアプリケーションの動作確認を行いたいと思います。
- SubstrateとInkの環境構築
- トークン生成、トークン移転およびApproveの機能を持つトークンコントラクト(NFTトークン)の作成
- Substrateブロックチェーン上でのスマートコントラクトのビルドとデプロイ
- Polkadot JS App を使用したアプリケーションの動作確認
Inkの導入
SubstrateおよびInkはRust言語上で動作します。Substrateのインストールについては以前の記事でも記載していますが再掲しておきます。既にSubstrateの環境がある方はRustの nightly
が最新であることと、WebAssemblyのビルドがサポートされていることを確認してください。Inkのスマートコントラクトは .wasm
ファイルにコンパイルされ、WebAssembly(Wasm)ランタイムとしてSubstrateチェーンへデプロイされます。[4]
Substrateのインストール方法です。
$ curl https://getsubstrate.io -sSf | bash
Ubuntu/Debian Linux detected.
Hit:1 http://jp.archive.ubuntu.com/ubuntu xenial InRelease
Get:2 http://jp.archive.ubuntu.com/ubuntu xenial-updates InRelease [109 kB]
Get:3 http://jp.archive.ubuntu.com/ubuntu xenial-backports InRelease [107 kB]
...
必要なライブラリやパッケージのインストールが始まり、Cargo/RustなどSubstrateのコンポーネントが順番にインストールされていきます。
これでインストール完了です。
Cloning into '/tmp/tmp.syyVafKCU8'...
remote: Enumerating objects: 48, done.
remote: Counting objects: 100% (48/48), done.
remote: Compressing objects: 100% (36/36), done.
remote: Total 169 (delta 25), reused 23 (delta 12), pack-reused 121
Receiving objects: 100% (169/169), 39.58 KiB | 0 bytes/s, done.
Resolving deltas: 100% (92/92), done.
Checking connectivity... done.
Run source ~/.cargo/env now to update environment
githubのsubstrate-upリポジトリからCLIのヘルパーをダウンロードしてきます。
$ git clone https://github.com/paritytech/substrate-up;
$ cp -a substrate-up/substrate-* ~/.cargo/bin;
$ rm -rf substrate-up
以下のコマンドで正しくインストールできていることを確認してください。
$ substrate --version
substrate 1.0.0-3ec6247-x86_64-linux-gnu
Rustの nightly
リリースをアップデートします。最初に rustup
がインストールされていることを確認します。
$ rustup -V
rustup 1.18.3 (435397f48 2019-05-22)$ rustup update nightly
wasm32-unknown-unknown
ツールチェーンを nightly
に追加してください。
$ rustup target add wasm32-unknown-unknown --toolchain nightly
info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date$ rustup target list | grep wasm
wasm32-unknown-emscripten
wasm32-unknown-unknown
InkおよびWebAssemblyに必要なライブラリをインストールします。 pwasm-utils-cli
はWebAssemblyのユーティリティツールです。2行目でInkを導入しています。CargoはRustにおけるパッケージマネージャです。
$ cargo install pwasm-utils-cli --bin wasm-prune
$ cargo install --force --git https://github.com/paritytech/ink cargo-contract
Substrateのノードが動くことを念のために確認しておきます。
$ substrate --dev
エラーが発生する場合は、以下のようにpurge-chain
を試してみてください。ブロックチェーンを初期化して、ブロック高『0』から再スタートできます。
$ substrate purge-chain --dev
Are you sure to remove "/.local/share/substrate/chains/dev/db"? (y/n)y
Inkプロジェクトのセットアップ
FlipperというシンプルなボイラープレートをフェッチしてInkプロジェクトを開始してください。[5]
$ cargo contract new flipper
flipperというフォルダが新規に作られ、以下のようなフォルダ構造で展開されるはずです。
Flipperはスマートコントラクトを作成するための基本的なファイルや構成が既に設定されているためすぐにInkによるスマートコントラクト開発に取り掛かることができます。
- プロジェクトのフォルダ構成とメタデータ設定がある
src/lib.rs
にサンプルのFlipperコントラクトがある、flip()
メソッドでtrue
/false
を反転させることが可能、またget()
メソッドでブロックチェーンから値を取得できるCargo.toml
ファイルがありプロジェクトの依存関係やモジュールのメタデータを定義済み、build.sh
はスマートコントラクトをコンパイルして.wasm
ファイル JSON abi などを作成できる
Inkは複数の抽象化レベルを持っています。下表はレベルを低いほうから高いほうへ上から順に並べています。 lang
というレベルがInkでスマートコントラクトを記述するのにあたり使用するモジュールでRustの『eDSL』に相当するレイヤーです。その配下に model
という中程度の抽象化したレイヤーと core
というユーティリティの層があります。
FlipperのプロジェクトをNFT(Non-Fungile Token)のプロジェクトへ作り変えます。
- フォルダ名を
nftoken
へ変更する
$ mv flipper nftoken
$ ls -al
Cargo.toml
の[package]
と[lib]
のname設定をnftoken
へ変更する
[package]
name = "nftoken"
version = "0.1.0"
authors = ["[your_name] <[your_email]>"]
edition = "2018"[lib]
name = "nftoken"
crate-type = ["cdylib"]
build.sh
のPROJNAME
にnftoken
を指定する
#!/bin/bash
set -ePROJNAME=nftoken
NFTokenコントラクト
Inkにおけるコントラクトの構造とEthereum/Solidityの構造は似ています(似せています)。InkのNFTokenのコードとSolidityの記法を比較しつつコードの理解を進めたいと思います。
コードのリポジトリです。[6]
以下がNFTokenのコードの外枠です。各番号の項目について以降コードを順番に確認しました。
// 1.モジュール宣言
use parity::<module>
...// 2.コントラクト展開
contract! { // 3.変数宣言
struct NFToken {
owner: storage::Value<AccountId>,
...
} // 4.コンストラクタ
impl Deploy for NFToken {
fn deploy(&mut self, init_value: u64){}
} // 5.イベント定義
event EventMint { owner: AccountId, value: u64 }
... // 6.メソッド定義
impl NFToken {
pub(external) fn total_minted(&self) -> u64 {}
...
} imp NFToken {
fn is_token_owner(
&self,
of: &AccountId,
token_id: u64) -> bool {}
...
}
}// 7.テスト
mod tests {
fn it_works() {}
...
}
1.モジュール宣言
外部のモジュールの読み込みです。Rustでは use
でモジュール読み込みができます。ink_coreとink_langから必要なモジュールを読み込んでいます。
parity_codec
から読み込んでいる Decode
および Encode
はイベントのフォーマット処理に使用します。
// Ink
use ink_core::{
env::{self, AccountId},
memory::format,
storage,
};use ink_lang::contract;
use parity_codec::{Decode, Encode};// Solidity
interface ContractName {
using SafeMath for uint256;
using AddressUtils for address;
}
以下のコードではfeatureフラグがstdのときかtestのとき以外は標準ライブラリを使用しないとしています。InkではRustの標準ライブラリを使用しないことを宣言しています。
#![cfg_attr(not(any(test, feature = "std")), no_std)]
2.コントラクト展開
Rustには構文レベルでコードを抽象化できる『マクロ』があります。Inkでは contract!
マクロでコントラクトを展開できるようになっています。Solidityでは contract
とコントラクト名を使用していて、見かけ上似ているので違和感なく使えるはずです。Inkでは contract!
を呼ぶだけです。
// Ink
contract! {
// contract
}// Solidity
contract <contract name> {
// contract
}
3.変数宣言
Inkでは変数はコントラクト名の構造体として宣言します。Solidityの mapping
と同様の辞書型(キーバリュー型?)を記述するには HashMap
を使用してください。変数は以降 self
で呼び出せます。
// Ink
/// Storage values of the contract
struct NFToken {
/// Owner of contract
owner: storage::Value<AccountId>,
/// Total tokens minted
total_minted: storage::Value<u64>,
/// Mapping: token_id(u64) -> owner (AccountID)
id_to_owner: storage::HashMap<u64, AccountId>,
/// Mapping: owner(AccountID) => tokenCount (u64)
owner_to_token_count: storage::HashMap<AccountId, u64>,
/// Mapping: token_id(u64) to account(AccountId)
approvals: storage::HashMap<u64, AccountId>,
}// Solidity
/// Owner of contract
address private owner;
/// Total tokens supplied
uint256 private totalSupply;
/// Mapping: token_id(u64) -> owner (AccountID)
mapping (uint256 => address) private idToOwner;
/// Mapping: owner(AccountID) => tokenCount (u64)
mapping (address => uint256) private ownerToCount;
/// Mapping: token_id(u64) to account(AccountId)
mapping (uint256 => address) private approvals;
Substrateのブロックチェーン上へデータを保存するためにink_coreにある storage
モジュールを使用しています。オンメモリで変数を扱うにはink_coreから memory
モジュールをロードします。
Solidityでは関数内の引数や変数に対して storage
や memory
型を指定し、コントラクトが変数を参照する場所を認識していました。
4.コンストラクタ
コードの記述は異なりますがコンストラクタの処理内容はどちらも同じです。Inkでは Deploy {}
ブロック内に deploy
関数を定義して引数および処理内容を記載します。
トークンの発行のために self.mint_impl
というプライベート関数を呼び出しています。Solidityでいうところのインターナル関数 _mint
を呼び出すイメージです。
// Ink
/// compulsary deploy method
impl Deploy for NFToken {
/// Initializes our initial total minted value to 0.
fn deploy(&mut self, init_value: u64) {
self.total_minted.set(0);
// set ownership of contract
self.owner.set(env.caller());
// mint initial tokens
if init_value > 0 {
self.mint_impl(env.caller(), init_value);
}
}
}// Solidity
constructor(uint256 initialSupply) public {
require(initialSupply > 0, "Initial supply is not sufficient");
totalSupply = 0;
owner = msg.sender;
_mint(msg.sender, initialSupply);
}
5.イベント定義
イベント定義はそれほど変わりません。Inkには indexed
にあたるような修飾子はないようです。イベントは env.emit
で呼び出すことができます。
// Ink
event EventMint { owner: AccountId, value: u64 }
event EventTransfer { from: AccountId, to: AccountId, token_id: u64 }
event EventApproval { owner: AccountId, spender: AccountId, token_id: u64 }// Solidity
event EventMint(address indexed owner, uint256 indexed value);
event EventTransfer(address indexed from, address indexed to, uint256 indexed _tokenId);
event EventApproval(address indexed owner, address indexed spender, uint256 indexed _tokenId);
6.メソッド定義
impl NFToken
のブロック内でパブリックおよびプライベートメソッドの定義をします。Rustでは明示的に指定しない限り関数、モジュール、トレイト、構造体はすべてデフォルトでプライベートです。Inkでは pub(external)
でパブリックメソッドが定義できます。
Solidityでは public
と internal
でパブリックとプライベートメソッドが定義できました。
// Ink
// public functions
impl NFToken {
pub(external) fn total_minted(&self) -> u64 {
let total_minted = *self.total_minted;
total_minted
}
}// private functions
impl NFToken {
fn mint_impl(&mut self, receiver: AccountId, value: u64) -> bool {}
}// Solidity
// public functions
function totalsupply() public constant returns (uint256 totalSupply) {
return (totalSupply);
}// private functions
function _mint(address account, uint256 amount) internal {}
トークンを作成する mint
を確認してみます。 env.caller()
や env.emit()
はink_coreで読み込まれています。 mint
はコントラクトのオーナーからしか呼び出せず、さらにはプライベートメソッドの mint_impl
をコールしてトークンを作成していることが分かります。トークン作成に成功したら EventMint
というイベントを呼び出して true
を返します。
引数の一つである &mut self
はミュータブルな参照によるBorrowingを行っています。
pub(external) fn mint(&mut self, to: AccountId, value: u64) -> bool {
if env.caller() != *self.owner {
return false;
} // carry out the actual minting
if self.mint_impl(to, value) == true {
env.emit(EventMint { owner: to, value: value });
return true;
}
false
}
Rustにおけるメモリ管理にはOwnershipとBorrowingという重要概念があるので理解しておくことをお勧めします。仕組みについてはこのブログが分かりやすかったです。[7]
owner の権限
- リソースがいつ解放されるかを制御出来る。
- リソースを immutable な形で多くの borrower に貸し与えることが出来る。
- リソースを mutable な形で1つの borrower に貸し与えることが出来る。borrower の権限
- borrowがimmutableなら、リソースの読み取りが出来る。
- borrowがmutableなら、リソースの読み書きが出来る。
- 他の誰かにリソースを貸し与える事が出来る。
参照の又貸しもできるみたいですね。アプリケーションロジックのコード部分は以上です。
7.テスト
テストが既にコントラクトコードに組み込まれています。Inkでは test
モジュールを読み込むことでコントラクトをビルドしてブロックチェーンへデプロイをすることなくアプリケーションロジックの実装をテストできます。
#[cfg(all(test, feature = "test-env"))]
mod tests {
use super::*; #[test]
fn it_works() {
let mut contract = NFToken::deploy_mock();
assert_eq(true);
...
}
}
テストの実行やコードのデプロイおよびアプリケーションのテストは次回で行います。以上です。
まとめ
- Ink(ink!)はSubstrateベースのブロックチェーンでスマートコントラクトを記述するための『eDSL』である
- EthereumのSolidityによるスマートコントラクトとコードの構造が似ているため実装しやすい
- RustのOwnershipやOptionなどの仕組みを理解しておく必要がある