Substrate & Ink Part 1 ~スマートコントラクト再入門~

Yuya Sugano
22 min readJun 16, 2019

--

ブロックチェーンの開発フレームワークである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のコンポーネントが順番にインストールされていきます。

One liner helps !!

これでインストール完了です。

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
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
Substrate node is now working !!

エラーが発生する場合は、以下のように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 boilerplate

Flipperはスマートコントラクトを作成するための基本的なファイルや構成が既に設定されているためすぐにInkによるスマートコントラクト開発に取り掛かることができます。

  • プロジェクトのフォルダ構成とメタデータ設定がある
  • src/lib.rsにサンプルのFlipperコントラクトがある、flip()メソッドで true / false を反転させることが可能、また get() メソッドでブロックチェーンから値を取得できる
  • Cargo.toml ファイルがありプロジェクトの依存関係やモジュールのメタデータを定義済み、build.sh はスマートコントラクトをコンパイルして .wasm ファイル JSON abi などを作成できる

Inkは複数の抽象化レベルを持っています。下表はレベルを低いほうから高いほうへ上から順に並べています。 lang というレベルがInkでスマートコントラクトを記述するのにあたり使用するモジュールでRustの『eDSL』に相当するレイヤーです。その配下に model という中程度の抽象化したレイヤーと core というユーティリティの層があります。

Ink Structure

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.shPROJNAMEnftoken を指定する
#!/bin/bash
set -e
PROJNAME=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では関数内の引数や変数に対して storagememory 型を指定し、コントラクトが変数を参照する場所を認識していました。

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では publicinternal でパブリックとプライベートメソッドが定義できました。

// 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などの仕組みを理解しておく必要がある

--

--

Yuya Sugano
Yuya Sugano

Written by Yuya Sugano

Cloud Architect and Blockchain Enthusiast, techflare.blog, Vinyl DJ, Backpacker. ブロックチェーン・クラウド(AWS/Azure)関連の記事をパブリッシュ。バックパッカーとしてユーラシア大陸を陸路横断するなど旅が趣味。

No responses yet