Substrateを使ってSexyに簡単なコントラクトを作成する


はじめに

こんにちは。
スタートバーン株式会社でブロックチェーンエンジニアをやっています。Sexydynamiteです。
今回はSubstrateでスマートコントラクトを書いていきます。
簡単なコントラクトを作成して開発の流れと概要をつかむことが目的です。
今回使用したコードはこちらに置いてあります。

ちなみに、弊社ではエンジニアを絶賛募集中です笑
https://www.wantedly.com/projects/388555
興味がございましたら、話だけでもお気軽に連絡ください。

Substrateとは

Parity Technologiesが提供するブロックチェーンを作るためのフレームワークです。
ブロックチェーンを実装するために必要な最低限の機能を提供してくれており、その他、自分で追加していきたい機能をモジュールとしてインポートして使います。
ちなみに、ブロックチェーンをSubstrateを使わずにスクラッチ実装しようとすると以下の項目を機能を自分で、実装する必要があります。

自分で一から実装するのも面白いとは思いますが、Substrateで実装した場合には、更に以下の恩恵を受けることもできます。

  • Polkadotのネットワークに接続できる
  • ハードフォークなしにアップデートできる
  • WebAssemlyのランタイムがある
  • Solidityのコントラクトをチェーン上で動かせる

上記にあげたものは主な例ですが、他にもEthereumのようにコントラクトのアップデートのたびに同じようなコードを何回もデプロイしなくてもよい設計などGavin氏の粋な計らいが随所に見られます。

ブロックチェーンをスクラッチ実装から運用までを自力でやるための技術力、セキュリティを担保するための資金力、そして開発時間を削減してくれる面白いツールです。

環境構築

Substrateでスマートコントラクトを書くためにink!と言うモジュールを使います。
ink!はRustコードからWebAssemblyのコントラクトを生成するためWebAssemblyの環境構築も併せて行う必要があります。

WebAssembly

Rustのアップデート

$ rust update nightly

wasmのインストール

$ rustup target add wasm32-unknown-unknown --toolchain nightly

Ink!

Substrateのインストール(20~30minかかります。)

$ curl https://getsubstrate.io -sSf | bash

Substrate起動(ノードが立ち上がったらインストールできています。)

$ substrate --dev

ink!のインストール

$ cargo install --git https://github.com/paritytech/cargo-contract cargo-contract --force

環境構築終了です。

プロジェクトの作成

ink!のプロジェクトを作成します。cargo contract new {project name} で実行します。

$ cargo contract new substrate_sample

ディレクトリ構成は以下のようになっています。

(substrate_sample)
--/.cargo
     |--config
--/.ink
     |--/abi_gen
          |--Cargo.toml
          |--main.rs
--lib.rs  <- コントラクトのコード
--Cargo.toml
--.gitignore

lib.rsにコントラクトを書いていきます。
デフォルトでサンプルコードが記述されているのでプロジェクトのルートディレクトリでコントラクトをテストしてみましょう。

$ cargo +nightly test
    Finished test [unoptimized + debuginfo] target(s) in 0.06s
    Running target/debug/deps/substrate_sample-150089c1acd8949d

running 2 tests
test substrate_sample::tests::it_works ... ok
test substrate_sample::tests::default_works ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

it_worksdefault_worksのテストにパスしていることがわかります。
failedが0であればテストはパスとなります。

コントラクトの記述様式

実際に開発を進める前に、lib.rsのコントラクトの中身をみてみましょう。(コメントアウトは消してあります。)

mod substrate_sample {
    #[ink(storage)]
    struct SubstrateSample {
        value: storage::Value<bool>,
    }

    impl SubstrateSample {
        #[ink(constructor)]
        fn new(&mut self, init_value: bool) {
            self.value.set(init_value);
        }

        #[ink(constructor)]
        fn default(&mut self) {
            self.new(false)
        }

        #[ink(message)]
        fn flip(&mut self) {
            *self.value = !self.get();
        }

        #[ink(message)]
        fn get(&self) -> bool {
            *self.value
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn default_works() {
            let substrate_sample = SubstrateSample::default();
            assert_eq!(substrate_sample.get(), false);
        }

        #[test]
        fn it_works() {
            let mut substrate_sample = SubstrateSample::new(false);
            assert_eq!(substrate_sample.get(), false);
            substrate_sample.flip();
            assert_eq!(substrate_sample.get(), true);
        }
    }
}

上記のコントラクトは4つのセクションに分けることができます。

  • ストレージの定義 -> #[ink(storage)]
  • コンストラクタの定義 -> #[ink(constructor)]
  • メソッドの定義 -> #[ink(message)]
  • テスト -> #[cfg(test)]

ストレージに変数を追加する場合は、コンストラクタでの変数の初期化が必須です。
次項では、実際にメソッドを追加します。4つの項目を編集して開発の流れをつかみましょう。

メソッドの追加

足し算をして合計値を変数に格納するメソッドを追加してみましょう。
ストレージに合計値を格納する変数を定義します。

    struct SubstrateSample {
        value: storage::Value<bool>,
        // 追加
        number: storage::Value<u32>,
    }

追加した変数をコンストラクタで初期化します。
初期化しないと変数を使うことはできません。
また、コンストラクタの引数にも初期値の0を追加しましょう。

        #[ink(constructor)]
                                                // 追加
        fn new(&mut self, init_value: bool, init_number: u32) {
            self.value.set(init_value);
            // 追加
            self.number.set(init_number);
        }
        #[ink(constructor)]
        fn default(&mut self) {
                         // 追加
            self.new(false, 0)
        }

足し算をして結果を変数に格納するメソッドを作成します。
テストで変数に格納されている値を取得するためのcheckメソッドも追加します。

        #[ink(message)]
        fn add(&mut self, a: u32, b: u32) {
            *self.number = a + b;
        }

        #[ink(message)]
        fn check(&self) -> u32 {
            *self.number
        }

最後にテストを追加してみましょう。

        #[test]
        fn default_works() {
            let substrate_sample = SubstrateSample::default();
            assert_eq!(substrate_sample.get(), false);
            // 追加
            assert_eq!(substrate_sample.check(), 0)
        }

        #[test]
        fn it_works() {
                                                                // 追加
            let mut substrate_sample = SubstrateSample::new(false, 0);
            assert_eq!(substrate_sample.get(), false);
            substrate_sample.flip();
            assert_eq!(substrate_sample.get(), true);
            // 追加
            substrate_sample.add(1, 2);
            assert_eq!(substrate_sample.check(), 3);
        }

テストの実行

$ cargo +nightly test
     Finished test [unoptimized + debuginfo] target(s) in 0.05s
     Running target/debug/deps/substrate_sample-150089c1acd8949d

running 2 tests
test substrate_sample::tests::default_works ... ok
test substrate_sample::tests::it_works ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

まとめ

いかがでしたか。
本来であればブロックチェーンの実装、スマートコントラクト言語の作成の工程を経て行うことのできるコントラクトの実装をインストール時間含め1時間少しでできるようになったことを考えるとよりブロックチェーンが身近になったのではないかと思います。
また、Substrate、Enigma、ZeroChain、Plasmaなどブロックチェーンソフトウェア関連のプロジェクトはRustで実装されておりそちらも楽しみです。
今回使用したコードはこちらに置いてあります。
最後までお読みいただきありがとうございました。

参照

ink! Smart Contracts Tutorial
Introducing Substrate Smart Contracts with Ink