2022.10.11
Substrate理解の第一歩:ink!によるコントラクト開発・Ethereumとの違い
はじめに
次世代システム研究室のY.C.です。ブロックチェーン開発フレームワークであるSubstrate。出来ることが多すぎて自分の中でブラックボックスと化していました。今回はとっかかりとしてink!というコントラクト記述言語を使ってSubstrateブロックチェーン用のWASMコントラクトを実装・デプロイするまでを試し、Ethereum(EVM/Solidity)との相違点を模索してみます。
結論
- 取っ付きづらさはあるが、やってみるとシンプルに開発できる
- Ethereumコントラクトと違って、インスタンスの実行コードそのものを入れ替えることが可能
- 対象チェーンが
pallet-contracts
を搭載していることが前提
動機
ink!
ink!を用いることで、Rustの安全性やエコシステムを利用出来ます。コントラクトはwasmにコンパイルされます。
https://use.ink/brand-assets/ink
かわいいイカちゃん、Squinkがマスコット。ink!の!を忘れると悲しみます。
WebAssembly(WASM)
wasmはネイティブなマシンコードに近く、高速に動作します。EVMは256bit stack machineという特殊すぎるアーキテクチャのため開発環境や解析ツールを独自に整える必要がありました。しかしwasmはブラウザ等幅広い環境で利用されており、様々なツールの恩恵を享受できます。
準備
初めにもろもろ必要なパッケージをインストールしていきます。最新版のドキュメントを参照ください。
https://use.ink/getting-started/setup
実装
プロジェクトの新規作成
先ほど導入したツールにより、コマンド一つでコントラクトのプロジェクトが生成できます。
cargo contract new hoge
雛形のコントラクトはbool値を一つ持ち、その値の反転と読み取りを行うための関数が定義されています。つまり最小構成でオンチェーンデータの読み書きが試せます。
型
Substrate contractでは Parity Codec としてエンコード可能な型を記録することが出来ます。これにはRustの基本的な型であるbool,u{8,16,32,64,128}, i{8,16,32,64,128}, String, タプル, 配列が含まれます。さらにSubstrate固有のプリミティブ型としてAccountId, Balance, Hashが利用可能です。またStorageに直接値を格納可能なMapping型が用意されています。
とはいえ、データの格納効率が悪いためStringの利用は推奨されません。どうしてもという場合はink_preludeというcrateを利用することでStringが使えるようになります。
Code hash
コンパイルされたwasmコードはhash値で識別することができます。この値を使って、コントラクト内で新たなコントラクトをインスタンス化することもできます。これはEVMにはない概念ですね。
Account ID
インスタンス化されたコントラクトはAccount IDで識別します。これを指定することでコントラクト同士でメッセージングできます。EVMにおけるaddressと同じですね。
Balance
EVMと同様に、コントラクトが残高を持っています。
Storage
オンチェーン上にデータを格納するStorageはkey-value databaseとなっています。keyのサイズは256bitで、ここまではEVMと同じ。valueのサイズに制限はないようです。使い勝手がよさそうですね。雛形コントラクトのように単純な変数を一つだけオンチェーンに記録するといったことももちろん可能ですが、内部的にはこのStorageに格納されます。
Storageの新たな領域を確保するにはdepositが必要で、領域を解放するとrefundされるのもEVMと同じです。
https://use.ink/datastructures/spread-storage-layout
Function
Message
パブリックなfunctionに#[ink(message)] を付与することで、呼び出し可能な API を定義できます。
読み込みでも書き込みでもcallしたい関数にはmessage属性をつけます。
#[ink(message)] pub fn get_hoge(&self) -> i32 { 1234567890 }
Payable
functionに#[ink(payable)]を付与することで、callすると同時に送金が可能になります。これはSolidityと同じですね。
Event
structに#[ink(event)]を付与し、emitすることでイベントを発行できます。この流れもSolidityと同じですね。
Selector
functionに#[ink(selector = S:u32)]を付与することで、セレクタを明示的に指定できます。セレクタとは、関数の識別子のことです。コンパイルされたコードには関数名の情報は残っていないため、この識別子を使って条件分岐することで指定した関数を実行することができます。
実はEVMバイトコードも同様に4byteの情報で実行する関数を識別しています。ただSolidityからコンパイルする際に関数名と引数の型をもとに自動的に決定されてされてしまいます。セレクタを明示するメリットとして、開発者が実行されるAPIを正確に制御できるようになり、APIの名前を変更しても処理が壊れないようになる、とのこと。
またセレクタとして_を指定することでfallback messageを指定できます。Ethereumコントラクトでのfallback関数といえば、悪用を防ぐためgas limit が制限され複雑な処理ができないようになっていました。Substrateのfallback messageでも同様に何かしら制限があると思ったのですが、執筆時点では特段そういった記載は見当たりませんでした。
#[ink(message, selector = 0xC0DECAFE)] fn my_message_1(&self) {} #[ink(message, selector = 42)] fn my_message_2(&self) {} #[ink(message, payable, selector = _)] fn my_fallback(&self) {}
https://use.ink/macros-attributes/selector
Test
コントラクトのモジュール内部に#[cfg(test)]をつけたテストモジュールを直接記述することができます。一つ外のスコープにあるコントラクト本体を参照するので、非常に簡潔に記述できます。テストはコンパイルせず実行できます。これは実に素晴らしいですね。コード例としては後述のコンパイルするコードをご覧ください。
コンパイル
#![cfg_attr(not(feature = "std"), no_std)] use ink_lang as ink; #[ink::contract] mod hoge { /// Defines the storage of your contract. /// Add new fields to the below struct in order /// to add new static storage fields to your contract. #[ink(storage)] pub struct Hoge { /// Stores a single `bool` value on the storage. value: bool, } impl Hoge { /// Constructor that initializes the `bool` value to the given `init_value`. #[ink(constructor)] pub fn new(init_value: bool) -> Self { Self { value: init_value } } /// Constructor that initializes the `bool` value to `false`. /// /// Constructors can delegate to other constructors. #[ink(constructor)] pub fn default() -> Self { Self::new(Default::default()) } /// A message that can be called on instantiated contracts. /// This one flips the value of the stored `bool` from `true` /// to `false` and vice versa. #[ink(message)] pub fn flip(&mut self) { self.value = !self.value; } /// Simply returns the current value of our `bool`. #[ink(message)] pub fn get(&self) -> bool { self.value } #[ink(message)] pub fn get_hoge(&self) -> i32 { 1234567890 } #[ink(message, selector = _)] pub fn my_fallback(&self) -> i32 { 1234567890 } } /// Unit tests in Rust are normally defined within such a `#[cfg(test)]` /// module and test functions are marked with a `#[test]` attribute. /// The below code is technically just normal Rust code. #[cfg(test)] mod tests { /// Imports all the definitions from the outer scope so we can use them here. use super::*; /// Imports `ink_lang` so we can use `#[ink::test]`. use ink_lang as ink; /// We test if the default constructor does its job. #[ink::test] fn default_works() { let hoge = Hoge::default(); assert_eq!(hoge.get(), false); } /// We test a simple use case of our contract. #[ink::test] fn it_works() { let mut hoge = Hoge::new(false); assert_eq!(hoge.get(), false); hoge.flip(); assert_eq!(hoge.get(), true); } } }
ターゲット
出力結果
コンパイルすることで以下の3点が出力されます。
- meatada.json: wasmコードのhash、コントラクトの名前やバージョン、関数名や引数/戻り値の型(ABI) を記録
- [contract name].wasm: wasmコードのバイナリファイル
- [contract name].contract: 16進表記のwasmコードとmetadata.jsonの内容を記録
出力されたhoge.contractの中身は以下のようになっていました。
いや長すぎやろ…バイトコードをディスアセンブリして処理を追いかけてみようと思っていましたが、これは厳しいです。
wasmをテキスト形式にしてみたところ、確かにマジックナンバーが埋め込まれていました。
デプロイ
デプロイはEVMと違い以下の2つのステップからなります。
- コードのアップロード:コンパイルして得られた.contractファイルをアップロードします。
- コードのインスタンス化:アップロードしたコードをもとにインスタンスを生成します。
一度アップロードしたコードを再アップロードすることなくインスタンスを再生成することが可能です。
デプロイ用のローカルチェーンを簡単に立ち上げるために、substrate-contracts-nodeが提供されています。macやlinuxであればバイナリファイルをダウンロードして実行するだけでいいので楽です。-lsync=debug オプションをつけるとデバッグログが出て賑やかになります。
今回はチェーンとのやり取りにcontracts-uiというツールを利用します。左上のnetowork欄でLocal Nodeを選択し、画面に従ってコードのアップロード・インスタンス化を行います。
ポチ。
Hogeコントラクトをインスタンス化できました。画像のように、オンチェーンデータの変更を伴う処理はgasを消費します。
message属性を付与した関数は呼び出せるようになっています。
オンチェーンデータを変更しない処理はgasが不要です。fallback関数も直接呼び出せるようです。
ところで、先ほどネットワーク欄の選択肢はこうなっていました。これらのネットワークは何でしょう?これしか選べないのでしょうか?
まずRococoは、parachainをテストするためのネットワークです。そしてContractsは、コントラクトをテストするためのparachainで、Rococo relay chainに接続しています。たくさんある他のparachainとContractsの違いは何でしょう?それは、pallet-contractsというpalletを搭載しているか否かです。palletというのはSubstrate ブロックチェーンに機能を追加することができるモジュールです。つまりコントラクトはSubstrateブロックチェーンが提供する機能の一部でしかないわけですね。もちろん単にリストに載っていないだけの可能性もありますが、他のチェーンはコントラクト実行環境を提供せず何を提供しているのでしょうか?ここにきて理解が進んだと同時に、また知らない宇宙に放り込まれてしまった気持ちになりました。
そしてShiden、Shibuyaはともに国産ブロックチェーンであるAstar Networkが提供するチェーンで、Shidenがカナリーネット、Shibuyaがテストネットです。メインネットであるAstarは??
https://docs.astar.network/docs/wasm/stack/nodes-clients#mainnet-node-astar
執筆時点(2022-10-11)で未実装でした。Q3は終わらない!
アップグレード
コントラクトをアップグレードする方法として2点挙げられています。
- プロキシフォワーディング
- set_code_hash()によるコントラクトコードの置き換え
1は各バージョンのコントラクトの前段にプロキシコントラクトをかませる手法で、EVMコントラクトでも利用されるパターンです。2はSubstrate contract 独自の機能です。一度デプロイしたコントラクトコードを変更できるというのはかなり強烈です。試してみます。
先ほどのHogeコントラクトにset_code関数を追加します。
Uploadable_hogeという名前でインスタンス化しました。upgradableじゃないのはご愛嬌。getHoge()の戻り値が1234567890となっています。
次に、get_hoge()の戻り値を987654321に変更したコードをアップロードします。現状contract-uiではコードをアップロードしてインスタンス化しないというのは出来ないようなので、一度別のコントラクトとしてインスタンス化します。
Uploadable_hogeのsetCode()をcallして、コードを置き換えます。ここで置き換えるコードのhashを指定するのですが、64文字の16進文字列を32個の1byte整数に変換して一つずつ入力する必要があり地味に大変でした。入力したそばから数字が書き変わったりするし。
置き換え後のgetHoge()です。確かに戻り値が987654321となっています。すごい。
コード置き換えによるアップグレードの注意点として、Storageへのアクセス順序が変わってしまうと既存データが壊れてしまうリスクがあります。プロキシパターンよりシンプルな構成になるのは間違いないですが、少し怖いですね。https://use.ink/basics/upgradeable-contracts/#storage-compatibility
まとめ
SubstrateブロックチェーンでWASMコントラクトを開発する流れを確認しました。初めはかなりとっつきにくさを感じていたのですが、振り返ってみるとツールも整っており開発しやすかったです。EVMコントラクトとの最大の違いは、コントラクトコードを置き換えることが可能な点です。テストが書きやすいのも大きなメリットです。pallet-contracts自体を改変することによりさらなる機能追加ができそうですが、同じコードならいつ・どこで実行しても同じ結果になることが求められるコントラクトの性質上、今まで出来なかったことが急に可能になるような劇的な変化はなかなか起こらないのではとも思いました。今後は実行性能や安全性についても調査していきたいです。
グループ研究開発本部 次世代システム研究室では、最新のテクノロジーを調査・検証しながらインターネット上の高度なアプリケーション開発を行うエンジニア・アーキテクトを募集しています。募集職種一覧 からご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD